feat(interactive): add interactive prompting for missing input across CLI domains#264
Open
iamfj wants to merge 13 commits into
Open
feat(interactive): add interactive prompting for missing input across CLI domains#264iamfj wants to merge 13 commits into
iamfj wants to merge 13 commits into
Conversation
✅ knip — no dead codeNo unused files, exports, types, or dependencies detected. |
Add a note in the root usage overview and the README example agent prompt telling agents to pass --no-interactive on every call.
… domains Add an interactive mode (-i/--interactive, --no-interactive) backed by a prompt engine, gating, and choice helpers using @clack/prompts. Wire interactive specs into issues, projects, labels, milestones, cycles, comments, documents, attachments, files, teams, and initiatives, and add a workflow-state-service plus supporting resolver/output/auth/error helpers.
28a928a to
3415583
Compare
…kers Extend the descriptor-driven prompt engine and finish wiring the domains that had drifted ahead of it, keeping the non-interactive JSON contract byte-identical. Engine/adapter: - add a `date` field kind: a segmented picker gated behind a confirm for optional fields so "leave unset" stays reachable, with no min/max so the interactive path matches the CLI's unconstrained date handling. - render a spec's intro lazily, exactly once, immediately before the first field that actually prompts (never when everything is skipped/provided). - factor the duplicated flat single-select pickers into a shared `makeChoicePicker` helper (pickers.ts). Per-domain wiring: attachments, comments, issues, labels, milestones, projects, and initiatives entity gain their create/update wizards and positional entity pickers, with the matching spec tests updated.
The base branch gained a teams write surface (create/update/membership) and initiative-update create/update after the prompt engine landed, leaving two create/update commands without a field wizard: they prompted only their positional/parent via a picker, so -i never gathered the actual fields — inconsistent with every other domain. teams: - add teamCreateSpec/teamUpdateSpec (name/key/description). Advanced boolean settings stay flag-only: parseBooleanOption trims its input and throws on a real boolean, so a confirm field would crash buildTeamFields (documented at the spec). - make `create <name>` optional (`[name]`) and fill it from the wizard text field, mirroring `labels create` (not `issues create`, which also gates on --team). Run the update wizard before the "at least one field" guard so prompted input counts. - demote add-member/remove-member `--user` from requiredOption to option and fill it via a user picker when absent on a TTY. initiatives updates: add body/health wizards for create and update. coverage-sweep: require each create/update file to reference a verb- matched *CreateSpec/*UpdateSpec. The prior file-level string match passed despite these missing wizards because both files already contained maybeCollectInteractive for their pickers. tests: team + initiative-update spec tests, plus non-TTY guards asserting a missing name / missing --user errors as JSON instead of hanging.
The descriptor-driven interactive rollout reached the primary create/ update/read paths but never covered a subsystem built out in parallel: the discussion thread/reply commands duplicated across issues, projects, and initiatives, plus relation and team-membership operations. In a terminal those commands still forced users to paste raw UUIDs — the exact friction the interactive feature exists to remove. Add a shared makeDiscussionPickers builder, colocated in commands/ (not common/interactive/, to preserve that layer's resolver-free invariant), that produces three positional pickers per content domain: - rootThreadPicker for reply/resolve/unresolve and thread reactions; - commentOrReplyPicker for edit/delete-comment, which the CLI accepts for a root OR a reply, so a root-only picker would silently drop reply targets; - replyPicker for edit-reply/delete-reply and reply reactions. Pickers are searchable, label threads by author/resolved state, and re-prompt on an empty entity instead of aborting mid-command. Shared resolvePickedPositional / resolveEmojiPositional / resolveDiscussionBody back every wired action; the required <thread>/<comment>/<reply> positionals become optional [..] so a picker can fire when the arg is omitted, while non-TTY/CI/piped runs keep the existing missing-argument error (they exit 1 with JSON, never hang or prompt). Also adds: issues relations add/remove pickers (relation labels narrow the forward/inverse union to both endpoints); teams remove-member offers the team's current members rather than all users; and field-wizard polish for attachments (comment/icon-url), projects (icon/hex color), documents (optional issue attach), plus a milestones-list project wizard replacing the hard --project requiredOption. unreact-id stays flag-only because its reaction id cannot be sourced, and the coverage-sweep allowlist is tightened to the genuinely-required positionals.
The `typecheck:test` CI job failed with six type errors in the new
interactive test suites.
Five came from `collectInteractive<O>`'s `provided: O` parameter. When a
test called it with a `{}` literal and a `PromptSpec<Opts>`, TypeScript
unified `O` down to `{}` (the empty-object candidate from the argument
won over the spec's `Opts`), so the returned draft had no known
properties and `result.team`/`result.title`/etc. were TS2339 errors.
`provided` is genuinely a partial set of already-supplied options, so
typing it `Partial<O>` both reflects reality and lets `O` be inferred
from the spec. The sole production caller passes a full `O`, which is
assignable to `Partial<O>`.
The sixth was a `noUncheckedIndexedAccess` violation: destructuring
`request.mock.calls[0]` (typed `any[] | undefined`) is not iterable.
Cast the tuple as the repo's other mock-call tests do, using an explicit
`{ filter?: unknown }` shape rather than `Record<string, unknown>` to
stay clear of `noPropertyAccessFromIndexSignature`.
The descriptor-driven interactive-prompt engine now spans src/common/ interactive/, wizard specs, and entity pickers across every write and read-by-id domain, guarded by a coverage-sweep test. AGENTS.md said nothing about it, so an agent adding a new command had no guidance on whether interactive support is required, what scope it takes (field wizard vs. entity picker), which shared helpers to reuse, or — most importantly — when NOT to prompt so the JSON/agent contract stays intact. The gap let commands ship without support (failing coverage-sweep) or invite hand-rolled prompts that break the machine contract. Add a concise, decision-oriented "Interactive Prompts" section covering: the input-only contract (stderr-only UI, byte-identical stdout JSON), a when-to-add decision list (create/update spec vs. optional [id] + picker vs. deliberate skip), the ~3-line call site, the shared choices/picker helpers to reuse, and the invariants plus the coverage-sweep enforcement. Cross-reference it from the Decision Tree and File Map. Docs-only. Verified the coverage-sweep claims by running that test, and had the section reviewed by accuracy and clarity subagents (findings folded in: the estimateChoices resolver exception, the "free-text positional" wording, and the Invariants→Rules heading to avoid colliding with the P0 Invariants section).
Several small correctness fixes to the interactive prompt layer, plus a
shared helper to remove duplication:
- Hoist the multiselect->CSV normaliser into the engine as
`normalizeWizardLists`. It previously existed twice (a `labels`-only
copy in issues.ts and a keyed copy in projects.ts); both command files
now import the single engine version. A `multiselect` yields a
`string[]`, but the command bodies expect the comma-separated string a
flag would give, so the join/delete has to happen in exactly one place.
- Seed the issue-create wizard with resolved team/project UUIDs before
running it, mirroring the update path. Without this, `-i --team ENG`
filtered the cycle/status/label/milestone loaders on the raw key and
silently offered no options. Resolution only runs when the wizard will
actually fire (guarded by shouldPrompt).
- Skip the emoji picker when `--shortcode` is given (comments and
discussion reactions). The shortcode already determines the emoji, so
prompting would force a glyph choice that then collides with the
shortcode in resolveReactionEmojiInput ("cannot provide both").
- Make document create/update's project (and create's team) optional in
the wizard via optionalChoices, so a document need not be tied to one.
Add rendered SVG demos of the two ways Linearis is driven — a human
stepping through the interactive issue-create wizard, and Claude Code
creating an issue via the skill — and rewrite the README opening to lead
with that human-vs-agent framing.
Supporting tooling and config:
- scripts/{rec-demo-interactive,gen-demo-agent,anonymize-demo}-cast.mjs
regenerate the casts; only the rendered SVGs are committed. The
intermediate .cast/.jsonl captures are regenerable, so .gitignore drops
them and knip ignores the one-off scripts (no importers by design).
- biome ignores docs/assets/*.svg (generated, not ours to format).
- SKILL.md notes the `--no-interactive` flag so an agent gets a JSON
error for a missing required argument instead of blocking on stdin.
The agent skill gained new guidance on this branch (documenting the `--no-interactive` flag for agents) but shipped without a version bump. Consumers pull skill and plugin updates by this SemVer version, which is independent of the date-based npm package version, so the content change needs to be reflected. Bump all four version fields in lockstep — SKILL.md metadata, plugin.json, and both fields in marketplace.json — as a patch, since the change is wording/guidance to existing content rather than new capability.
Future coding agents had no instruction to bump the skill/plugin version when changing shipped content, so edits landed without one (this branch's --no-interactive guidance among them). Add a "Skill & Plugin Versioning" section explaining the SemVer version is independent of the date-based npm version, that all four version fields must move in lockstep, and how to pick patch/minor/major. Also list skills/ and .claude-plugin/ in the File Map so the files are discoverable.
The `issues update` wizard reused the create-oriented choice loaders for the assignee and project fields, so their escapable sentinel rendered as "None (unassigned)" / "None (no project)". Selecting an empty-valued sentinel means "leave unset", which on update leaves the field unchanged rather than clearing it — so the labels implied an outcome the engine never performs, and there is no clear path from the wizard. The sibling update fields (milestone/cycle/status/estimate) already relabel their sentinel "Keep current" for exactly this reason; assignee and project were missed. Switch both to `optionalChoices(..., "Keep current")`, preserving the project loader's team-scoping. Also widen the `issues create` action generic from `[string, ...]` to `[string | undefined, ...]` to match the now-optional `[title]` positional (and the other create commands). Previously `title` was typed as `string`, making the runtime `title === undefined` guard dead per the type system.
… update-spec docs Follow-up fixes to the interactive-prompts feature raised in review. Crash fix: clack's single-select indexes options[cursor] in its constructor, so calling io.select on an empty option list throws a raw TypeError instead of the JSON error contract. Add the same empty guard the comments/attachments/discussion pickers already use to every remaining picker that can face an empty list (makeChoicePicker plus the milestone, cycle, label, initiative-update, project, document, and initiative entity/relation pickers). Team/user pickers stay unguarded because a workspace always has at least one team and the current user. Ordering: resolve the positional picker before running the field wizard in maybeCollectInteractive, so an interactive user chooses which entity to act on before being prompted for its fields. Fixing it in the shared helper covers every update command (and the create commands with a positional picker) at once instead of per command. Docs: drop the inert default callbacks from documentUpdateSpec and correct the "current option values seed each field" comments across the update specs. The wizard skips a flag-supplied field and prompts the rest fresh; it does not pre-load an entity's current values. Tests: add makeChoicePicker empty-guard coverage, a positional-before- fields ordering test, and update the documentUpdateSpec test that asserted the removed default.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds an interactive mode to the CLI (
-i/--interactiveand--no-interactiveglobal flags) that prompts for missing input using@clack/prompts. The core lives insrc/common/interactive/(prompt engine, gating, choice helpers, clack I/O adapter, types), backed by a newworkflow-state-service, and is wired into the issues, projects, labels, milestones, cycles, comments, documents, attachments, files, teams, and initiatives commands with per-domain interactive specs and supporting resolver/output/auth/error helpers. Comprehensive unit tests cover the engine, gating, choices, and each domain's specs.Type of change
Checklist
npm run check:cipasses (lint + format)npx tsc --noEmitpasses (type check)npm testpasses (unit tests)Testing
Ran
npm run check:ci,npx tsc --noEmit, andnpm test— all pass.Notes for reviewers
JSON output remains the default and unchanged when non-interactive; prompting only engages with
-i/--interactive(or a TTY) and is fully disabled with--no-interactive, keeping agent usage stable.