Skip to content

feat(interactive): add interactive prompting for missing input across CLI domains#264

Open
iamfj wants to merge 13 commits into
nextfrom
conductor/iamfj-cli-interactive-domains
Open

feat(interactive): add interactive prompting for missing input across CLI domains#264
iamfj wants to merge 13 commits into
nextfrom
conductor/iamfj-cli-interactive-domains

Conversation

@iamfj

@iamfj iamfj commented Jul 3, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds an interactive mode to the CLI (-i/--interactive and --no-interactive global flags) that prompts for missing input using @clack/prompts. The core lives in src/common/interactive/ (prompt engine, gating, choice helpers, clack I/O adapter, types), backed by a new workflow-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

  • New feature

Checklist

  • npm run check:ci passes (lint + format)
  • npx tsc --noEmit passes (type check)
  • npm test passes (unit tests)
  • New code has tests (happy path + primary error case)
  • Commit messages follow Conventional Commits

Testing

Ran npm run check:ci, npx tsc --noEmit, and npm 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.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

✅ knip — no dead code

No unused files, exports, types, or dependencies detected.

iamfj added 2 commits July 4, 2026 19:24
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.
@iamfj iamfj force-pushed the conductor/iamfj-cli-interactive-domains branch from 28a928a to 3415583 Compare July 4, 2026 17:26
iamfj added 11 commits July 4, 2026 20:08
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant