Skip to content

fix(ask-user): key AskUserQuestion answers by question text for SDK schema#359

Closed
tusharsingh wants to merge 33 commits into
chadbyte:mainfrom
tusharsingh:main
Closed

fix(ask-user): key AskUserQuestion answers by question text for SDK schema#359
tusharsingh wants to merge 33 commits into
chadbyte:mainfrom
tusharsingh:main

Conversation

@tusharsingh
Copy link
Copy Markdown

@tusharsingh tusharsingh commented May 19, 2026

For the Claude native AskUserQuestion path (canUseTool), Clay was forwarding the UI's numeric-index answers map
verbatim to the Agent SDK as updatedInput.answers. The SDK's AskUserQuestionOutput schema keys answers by
question text, so the SDK couldn't match any answer back to its question and the model received blank answers —
surfacing as "I don't see an answer" or no acknowledgement of the user's selection.

Repro: trigger an AskUserQuestion from any Claude-vendor session (mates are unaffected — see below), select or type
any answer, observe the model respond as if no answer was given.

Root cause

The UI in lib/public/modules/tools.js builds the answers object keyed by numeric index:

// lib/public/modules/tools.js
result[i] = answers[i];   // → { "0": "...", "1": "...", "2": "..." }

The server then passed this straight through in lib/project-sessions.js:

pending.resolve({
  behavior: "allow",
  updatedInput: Object.assign({}, pending.input, { answers: answers }),
});

But the Agent SDK's type for AskUserQuestionOutput (@anthropic-ai/claude-agent-sdk/sdk-tools.d.ts) declares:

/**
 * The answers provided by the user (question text -> answer string;
 * multi-select answers are comma-separated)
 */
answers: { [k: string]: string };

So the SDK was looking up answers["How should we handle X?"] and finding undefined — the model rendered a blank tool
result.

Fix

Remap numeric-index answers to {questionText: answer} using pending.input.questions[i].question as the key before
resolving the canUseTool promise. Falls back to "Question N" only if the question text is missing.

What's not affected

The MCP/mates path (lib/project.jsclay-ask-user MCP server) is untouched. It builds a plain-text user message via
formatAskUserAnswerAsMessage and never hands an answers object to the SDK's schema, so the mates flow has always
worked correctly.

Test plan

  • Reproduced blank-answer symptom on main with a 3-question card (one free-text "Other" answer, two option picks).
  • Server-side diagnostic logs confirmed answers were received and forwarded correctly — the SDK was the consumer
    dropping them due to the key mismatch.
  • Applied fix; same repro now shows the model correctly mapping each user answer to its question.
  • Spot-check the MCP/mates ask_user_questions flow still works (formatter is untouched but worth confirming).

tusharsingh and others added 30 commits May 19, 2026 10:56
…tput

The canUseTool path forwarded the UI's numeric-index answers map
({"0": "...", "1": "..."}) straight to the SDK as updatedInput.answers.
The Agent SDK's AskUserQuestionOutput schema keys answers by question
text, so the SDK could not match any answer back to its question and
the model received blank answers — visible as "I don't see an answer"
or no acknowledgement of the user's selection.

Remap numeric-index answers to {questionText: answer} before resolving
the canUseTool promise. The MCP/mates path is unaffected: it builds a
plain-text user message and never touches the SDK answers schema.
…on-text

fix(ask-user): key answers by question text for SDK AskUserQuestionOutput
…estart

session.titleManuallySet (set on rename_session) and titleAutoGenerated
(set by the auto-title pass) gate the AUTO_TITLE_TURN_THRESHOLD check in
sdk-message-processor.js, but neither flag was serialized in
saveSessionFile / loadSessions. After any daemon restart the flags were
lost while session.title persisted, and the next time the session
crossed turn 2 the auto-title overwrote the user's rename.

Persist both flags in the session meta and restore them on load. Add a
backwards-compat shim so sessions whose files predate this change but
already carry a non-default title also skip the next auto-title pass.
…name

fix(sessions): persist title-origin flags so manual renames survive restart
A single tool, spawn_session, lets an agent in a Clay session create new
sessions in the current project and seed each with an initial user
message. Returns immediately so the caller can fan out multiple sessions
in one turn — primary use case is splitting a planning session's list of
work items (e.g. JIRA issues) into per-item sessions, each started with
the appropriate slash command (e.g. "/jira HARD-207"). Slash commands
pass through verbatim to the SDK.

The new session's title is marked titleManuallySet=true so Clay's
auto-title pass at AUTO_TITLE_TURN_THRESHOLD does not overwrite it.

Available in main project sessions (not mates), matching the scope of
clay-debate. Inputs: title, initial_prompt, optional vendor (defaults to
"claude").
Beef up the tool description so the agent calls spawn_session on its own
when the user says things like "start working on these in clay", "split
these into sessions", or "open a session for each of these" — instead
of requiring the user to point at the tool by name.

Adds explicit trigger phrases, behavior guidance (call in parallel, no
confirmation prompt, brief post-spawn summary), title-selection rules
(use the item identifier), and initial-prompt selection (prefer a
project slash command like "/jira <KEY>" when one fits, fall back to a
short natural-language instruction).
When the user said "Start working on these issues in phase 1 in clay",
the agent picked the clay-ralph skill (which claims phrases like "I want
to automate this task" or "run overnight") instead of spawn_session.

Tighten the tool description so the agent reliably picks spawn_session
for any "list of items → list of sessions" request:

- Call out clay-ralph by name in a DISAMBIGUATION block: ralph is for a
  single autonomous loop against ONE prompt/judge pair; spawn_session is
  for splitting a list of work ITEMS into per-item interactive sessions.
- Add explicit triggers for "work on these in clay", "do these in clay",
  "one session per issue", and "phase 1/2/..." phrasings.
- Restrict clay-ralph to explicit "ralph"/"autonomous loop"/"AFK" cues.
The clay-tools stdio bridge at lib/yoke/mcp-bridge-server.js was built
for Codex; reuse it to surface Clay's in-app MCP servers (clay-sessions,
clay-debate, clay-history, clay-ask-user) inside TUI Claude sessions
too. Without this, a TUI session only sees file-based skills like
/clay-ralph — MCP tools were silently absent because Clay never wired
its servers into the claude CLI's MCP config.

Changes:

- lib/tui-mcp-config.js: helper that writes a temp --mcp-config JSON
  pointing at the clay-tools bridge for the daemon's port/slug/auth,
  with a matching cleanup hook. Shared by all TUI launch paths.

- lib/project-sessions.js: both TUI launch sites (new-TUI-session and
  runtime-resume PTY) now append --mcp-config and remove the temp file
  on PTY exit.

- lib/spawn-mcp-server.js: spawn_session accepts mode ("tui" | "gui"),
  defaulting to "tui" so per-issue sessions inherit subscription billing.
  Tool description gates "gui" mode to explicit user requests or codex.

- lib/project.js: onSpawn callback branches on mode. TUI spawn
  pre-assigns the cliSessionId and launches
    claude --session-id <uuid> -n "<title>" --mcp-config <cfg> "<prompt>"
  in an xterm via tm.create. GUI spawn keeps the existing SDK flow.
When spawn_session created a TUI session, it launched the claude CLI
with no attached client. claude exited almost immediately ("no
interactive partner"), the PTY closed, onExit fired, and the session
was deleted — making the spawned session appear in the sidebar for a
few seconds and then disappear.

Defer the PTY launch. spawn_session now creates the session record
with mode=tui, cliSessionId preassigned, and pendingInitialPrompt
stored. The PTY is launched in project-sessions.js switch_session when
the user first opens the session, with the active ws as the PTY owner.
By that point there is an attached client, claude stays interactive,
and the conversation runs normally.

Also derive the session title from the leading slash command's first
positional arg (e.g. "/jira GP-222" -> "GP-222") so the sidebar title
matches the issue identifier regardless of what the calling agent
passed. Falls back to the agent-supplied title for free-form prompts.

Persist pendingInitialPrompt across daemon restarts so a spawned
session that the user opens after a restart still seeds claude with
the right first message.

On PTY exit, no longer delete the session: the conversation is saved
under ~/.claude/projects/<cwd>/<uuid>.jsonl and the user can re-open.
Three gaps left TUI sessions invisible (or dead) after a daemon restart:

1. lib/project-sessions.js new_session TUI path created the session via
   createSessionRaw + switchSession but never called saveSessionFile, so
   no record was written to ~/.clay/sessions/<slug>/<uuid>.jsonl and
   loadSessions had nothing to bring back.

2. saveSessionFile/loadSessions did not include session.mode in the meta
   block, so any TUI session that did get persisted (e.g. via the spawn
   flow or via a later save) came back tagged as gui. The
   deferred-launch check in switch_session keys off mode === "tui" and
   would silently skip the relaunch.

3. Even with mode restored, a freshly loaded TUI session has no
   in-memory terminalId (terminals are not persisted). The previous
   deferred-launch block only handled the spawn-first-open case
   (pendingInitialPrompt). Post-restart sessions have no pending prompt
   but still need a PTY.

Changes:

- lib/sessions.js: meta.mode = "tui" written/read; mode is the only
  non-default value worth persisting (gui is implicit).
- lib/project-sessions.js: saveSessionFile after TUI new_session
  creation so the record hits disk.
- lib/project-sessions.js switch_session: the deferred-launch block now
  branches — pendingInitialPrompt -> claude --session-id <uuid> with the
  prompt; otherwise -> claude --resume <uuid>. Either way the PTY only
  launches when the user actually opens the session, with an attached
  ws, so claude stays interactive.
claude's --mcp-config flag is variadic (<configs...>) so it greedily
consumes following non-flag args. A spawn_session command like

  claude --session-id <uuid> -n 'GP-222' \
    --mcp-config '/tmp/clay-tui-mcp-xxx.json' '/jira GP-222'

was being parsed as two MCP config files. The CLI rejected the launch
with "MCP config file not found: /jira GP-222".

Insert "--" between the options list and the positional prompt so the
prompt isn't swept into --mcp-config.
Defer-until-click was a workaround for sessions disappearing when their
PTY exited immediately — but that root cause was the variadic
--mcp-config swallowing the positional prompt (fixed in c73ccea), not
"no client attached". Restore eager launch so spawn_session actually
fans out work in parallel: the user calls the tool, claude starts
running /jira <KEY> in each spawned session in the background, and the
user can come back later to see results.

The deferred-launch path in switch_session still handles the post-exit
re-open case (terminalId cleared on PTY exit, next click spawns
`claude --resume <uuid>`), so subsequent opens resume the persisted
conversation rather than starting a new one.
The agent's title arg is now overridden by the first positional
argument of any leading slash command (e.g. \"/jira GP-222\" -> the
title becomes \"GP-222\"). Document this in the tool description so
the agent passes the right shape and isn't surprised when its title
arg gets replaced.
Add permission_mode and effort args to spawn_session. Both default
toward planning-shaped per-issue work:

  - permission_mode defaults to "plan" so each spawned session loads
    context with the slash command, produces a plan, and waits for the
    user before editing.
  - effort defaults to "high" so the planning pass reasons carefully.

Wired into the TUI launch as --permission-mode <mode> --effort <level>.
GUI sessions don't honor these args yet (the SDK adapter reads effort
and permission mode from per-session state at startQuery time and there
is no clean hook from the MCP onSpawn handler); spawned GUI sessions
inherit the daemon-level defaults for now.

The tool description for both args calls out the planning-shaped
default and the override path, so non-planning spawns can opt out.
spawn_session previously returned a free-text "Spawned session #N \"T\""
line, which forced the dispatcher agent to parse the localId out of
prose and gave it no way to refer back to the spawned session's claude
UUID. Two consequences:

  - No clean handle to pass to future tools like mark_session_done,
    send_to_session, or any other operate-on-session helper.
  - No claude --resume target the dispatcher could surface to the user
    ("session #5 is GP-222, resume from the terminal with claude
    --resume <uuid>").

Return an additional content block per call:

  [clay-sessions/spawn_session] localId=42 cliSessionId=50a893... \
    title="GP-222" mode=tui vendor=claude

The summary line is kept for human-readable rendering. The onSpawn
callback's resolved value now includes cliSessionId, mode, and vendor
so the MCP wrapper can render the tracking line without re-deriving.
TUI sessions have cliSessionId from spawn-time (we preassign); GUI
sessions report cliSessionId=(pending) until the SDK emits init and
sm.saveSessionFile updates the record.
Companion to spawn_session for closing out work. The user's /done
skill (or any agent) calls mcp__clay-sessions__mark_session_done with
an optional session_id; Clay prefixes the matching session's title
with "done - " (idempotent) and broadcasts the sidebar update so the
user sees the session visually marked.

- session_id omitted: operates on the currently active session (the
  natural mode for /done invoked from inside the session being closed).
- session_id from a dispatcher's tracked spawn_session call: closes a
  peer session without switching to it.
- undo: true reverses the prefix to recover the original title.

Title-origin flags carry over: titleManuallySet is set so Clay's
auto-title pass cannot overwrite the marker. SDK rename is broadcast
to claude so the prompt-box display name (-n) stays in sync.

Tool result mirrors spawn_session's shape: a human-readable summary
plus a [clay-sessions/mark_session_done] tracking line carrying
localId / cliSessionId / new title / action / changed. The dispatcher
keeps a structured handle to refer back to the affected session.
feat(sessions): spawn_session MCP tool + TUI MCP wiring
…P bridge

Surfaces the new spawn_session / mark_session_done workflow and the
TUI MCP bridge so users can find them without reading the source:

- Top-level "What's Clay?" bullet now mentions fan-out alongside Ralph
  Loop and cron.
- New "Session fan-out: one prompt, many sessions" section with the
  literal trigger sentence, the per-ticket title + auto-loaded /jira
  context + plan-mode-at-high-effort behaviour, and the /done close-out
  flow that flips JIRA status and prefixes the Clay sidebar entry.
- MCP FAQ updated to list the new "sessions" built-in server and the
  TUI bridge that exposes Clay's in-app servers to the real claude CLI
  via --mcp-config.
- Architecture diagram's Built-in MCP servers node updated to include
  "sessions" next to ask-user / browser / debate / email.
The TUI session host computed its bottom edge from window.innerHeight,
which iOS Safari (and the PWA standalone runtime) leaves at the full
viewport height even while the on-screen keyboard is up. The lower
portion of the xterm slid behind the keyboard with no way to bring it
back without dismissing input.

Switch the bottom-edge calculation to window.visualViewport.offsetTop +
visualViewport.height, which tracks the visible area above the
keyboard and accounts for any layout shift iOS applies. Subscribe the
view to visualViewport's resize and scroll events too — neither show
up on the regular window resize listener — so the host re-fits and
fitAddon recomputes cols/rows when the keyboard slides in or out.
…ive one

/done from a TUI session marked the WRONG session done when the user
switched sessions while claude was mid-/done (e.g. waiting on a JIRA
transition). mark_session_done's fallback was sm.getActiveSession(),
which returns whichever session the user is currently viewing — so by
the time the tool fired, "active" had moved to the user's new session
and got the "done - " prefix.

Plumb the calling session's cliSessionId from the TUI bridge through
to the MCP tool handler so it's deterministic:

- lib/yoke/mcp-bridge-server.js: accept --session-id <uuid>, include
  it as callingCliSessionId in every call_tool POST body.
- lib/tui-mcp-config.js: buildTuiMcpConfig now accepts a cliSessionId
  arg and adds --session-id to the bridge launch args.
- lib/project-sessions.js + lib/project.js: all four TUI launch sites
  (new TUI session, runtime resume PTY, switch_session deferred
  re-open, spawn_session) pass the session's cliSessionId through.
- lib/project-http.js: /api/mcp-bridge call_tool extracts
  callingCliSessionId from the body and injects it into the tool args
  as __callingCliSessionId.
- lib/sessions.js: new findSessionByCliSessionId() helper.
- lib/project.js onMarkDone: resolves the target in priority order —
  explicit session_id > __callingCliSessionId lookup > getActiveSession.

GUI MCP tool calls don't go through the bridge, so they still fall back
to getActiveSession; that path doesn't have the same async race because
GUI tool calls are in-process and the session is identifiable from the
SDK invocation context. A future change can plumb the calling session
into GUI tool handlers too.
Two ways to hand a file's server-side path to the TUI's claude CLI:

  - Drag-and-drop onto the xterm host (desktop). A dashed outline
    appears while a file is dragged over.
  - A floating "+" button at the bottom-right of the host, sized for
    touch. On iOS it opens the native picker which offers both Photo
    Library and Files; on Android the equivalent picker.

Both paths read each selected/dropped file as base64, POST it to the
existing /api/upload endpoint (which writes it to /tmp/clay-<hash>/
and returns an absolute path on the server), then send the
space-separated paths to the PTY as a single term_input frame. The
paths arrive at claude's prompt exactly as if the user had typed
them, so they append to whatever the user was composing and can be
referenced (e.g. claude Read, attach as image) per its usual rules.

Drag and drop events aren't fired on touch devices, so mobile uses
the + button only. The button sits inside hostEl, which is sized
above the iOS keyboard via visualViewport, so it stays reachable
when the keyboard is up.

Uploads run in parallel; paths are batched into one PTY write so
they appear at one cursor position rather than racing as each
upload completes.
spawn_session derives the session title from the leading slash command's
first positional arg ("/jira GP-222" -> "GP-222"), which keeps the
sidebar clean but loses context. A skill that has just fetched the
issue summary now has a way to refine the title to something like
"GP-222 - Implement OAuth refresh".

rename_session({title, session_id?}) sets the title on a session.
session_id is optional; omitted it routes to the calling session via
the same __callingCliSessionId path mark_session_done already uses,
so the rename always hits the session that invoked the tool — not
the user's globally-active view.

Refactored onMarkDone to share resolveTargetSession() with the new
onRename so both apply the same priority: explicit session_id >
__callingCliSessionId > getActiveSession().

The new title is marked titleManuallySet=true so Clay's auto-title
pass at AUTO_TITLE_TURN_THRESHOLD doesn't overwrite it; the SDK
rename is broadcast so the claude prompt-box display name stays in
sync.
Now that claudeOpenMode defaults to tui and spawn_session creates TUI
sessions by default, the "terminal" badge appeared on basically every
sidebar row and stopped carrying signal. Removing it matches what the
session list already implicitly conveys.

The reload-then-icon-disappears asymmetry the user noticed (mode wasn't
persisted before, then was, so badges came and went across restarts)
also goes away with the badge.
…ions show

The Import CLI picker called adapter.listSessions(cwd) and only fell
back to the FS-based parser when the SDK call threw. In practice the
SDK's listSessions returns a strict subset of what's on disk for some
projects — conversations claude --resume happily finds via direct
.jsonl scan never appeared in Clay's Import CLI even though they
weren't tracked by Clay yet. Users couldn't bring orphaned claude
sessions back into Clay's sidebar.

Union the two sources: always run cli-sessions.listCliSessions (which
parses every .jsonl in ~/.claude/projects/<encoded-cwd>/), then
overlay richer metadata from the SDK where present. The filter that
hides sessions already known to Clay still runs.

After this change, anything `claude --resume` can resume is reachable
from Clay's Import CLI too. In particular: spawn-created TUI sessions
whose Clay record was wiped by older deletion-on-exit code (before
7d0c006) can be recovered by clicking them in Import CLI.
Default behaviour for Import CLI is back to the original — list the
sessions the Agent SDK enumerates for this cwd, hiding any UUIDs Clay
already tracks. Quiet, fast.

Add a "Show all" checkbox in the picker that, when checked, also
scans the filesystem at ~/.claude/projects/<encoded-cwd>/ and unions
the results. That surfaces conversations the SDK doesn't list —
typically orphaned sessions (e.g. TUI records wiped by pre-7d0c006
deletion-on-exit code) — so the user can recover them.

Server: list_cli_sessions accepts an optional show_all flag and runs
the filesystem scan only when true.
Client: HTML checkbox, JS that re-fires list_cli_sessions on toggle,
small CSS to host the new controls row.
TUI sessions don't route their typing through Clay (claude writes the
jsonl to its own per-cwd store), so once the meta file was saved at
spawn time the sidebar timestamp never moved — a session created
yesterday and used heavily today still rendered as "yesterday".

Two new fresh-activity signals:

- switch_session: forced bump on open. Clicking into a session is a
  strong "I'm using this now" signal regardless of whether the user
  goes on to type. Forced persistence so the sidebar reorders right
  away.
- term_input: bump per keystroke / chunk written to the TUI's PTY,
  throttled to one meta-file write per 60 seconds per session so we
  don't thrash disk on every byte.

sessions.js gets two helpers: findSessionByTerminalId (the term_input
handler doesn't otherwise know which session owns a terminal) and
bumpSessionActivity(localId, {force}) which centralises the throttle.

GUI sessions still update via the existing message-flow code paths;
this change only fixes the gap for TUI.
…current

Bumping on session open meant a user just browsing through TUIs would
mark them all "today" even though they did nothing. Per user request,
revert that part — only PTY input (term_input) bumps lastActivity now.

The 60s throttle in bumpSessionActivity still applies, so heavy typing
doesn't thrash the meta jsonl. First keystroke after a long idle still
forces a persist because _lastActivityPersistedAt is unset, giving
immediate sidebar reorder once the user actually engages.
Replace the "done - " title prefix with a real boolean on the session
record. The sidebar now has Active and Completed tabs that partition
sessions by the flag; tag counts on each tab reflect what's hidden.

Backend:
- lib/sessions.js: persist meta.done in saveSessionFile, read it in
  loadSessions, and migrate legacy "done - "-prefixed titles on load
  (strip the prefix, set done=true). The migration runs only when
  meta.done isn't already recorded, so it's idempotent.
- lib/sessions.js mapSessionForClient: include done in the broadcast
  payload so the sidebar can filter without an extra round trip.
- lib/project.js onMarkDone: flip session.done instead of mutating
  title. undo:true now clears the flag back to false.
- lib/spawn-mcp-server.js: tool description + summary updated to talk
  about a flag, not a prefix.

Frontend:
- lib/public/modules/sidebar-sessions.js: module-level sessionListTab
  ("active" by default), renderSessionTabBar() with counts, filter
  items by tab before partitioning into bookmarked/regular. Clicking
  a tab re-renders. Loop groups always live in Active.
- lib/public/css/sidebar.css: tab bar styling (subdued chips, count
  badge highlight on the active tab).

Existing sessions with "done - X" titles get auto-migrated to
done=true with the prefix stripped on next load — the user's accumulated
backlog of completed JIRA work shows up in Completed without manual
re-tagging.
Context menu now has an explicit toggle between Active and Completed
that doesn't require invoking the /done skill. Mirrors what
mark_session_done does from the MCP side: flips session.done, saves,
broadcasts. The label and icon swap based on the session's current
state ("Mark done" with a check / "Mark active" with a back-arrow).

Adds set_session_done to ws-schema as a c2s message.
There are two session_list construction paths: broadcastSessionList in
sessions.js calls mapSessionForClient and includes all fields; the
initial-connection sender in project-connection.js (lines 165-189) had
its own hand-rolled payload that was a strict subset.

The on-connect payload was missing the new "done" field, so after a
restart the browser's first session_list arrived with done=undefined
on every session and the new Active/Completed tabs ignored the
migration. Subsequent broadcasts wouldn't fire until something
mutated, so the user was stuck looking at everything in Active even
though loadSessions had correctly set done=true on 227 sessions.

Pull the missing fields (done, mode, vendor, terminalId, runtimeMode,
runtimeTerminalId) into the on-connect payload so the initial render
matches what later broadcasts contain.
Add a top-level skills/ directory with ready-to-install reference
implementations of the two Clay-flavoured slash commands the README's
"Session fan-out" section assumes:

  - skills/jira.md: the full JIRA -> implementation workflow. Fetches
    the issue, renames the Clay session to "<KEY> - <summary>",
    transitions In Progress, dispatches parallel subagents for
    codebase exploration, produces a plan with two approval gates,
    implements, transitions to Done.
  - skills/done.md: wraps up the session. Transitions the JIRA ticket
    and calls mark_session_done, which flips the structural flag (no
    longer prefixes the title) so the sidebar moves it from Active to
    Completed automatically.
  - skills/README.md: install instructions (user-level via
    ~/.claude/commands/ or project-level via .claude/commands/) plus
    pointers to the Clay MCP tools the skills depend on.

Main README's Session fan-out section updated to:
  - mention rename_session alongside spawn_session / mark_session_done
  - describe the done flow as moving sessions to the Completed tab
    (instead of the old "done - " title-prefix wording)
  - link to skills/ as the install path.

The shipped /done text uses the new flag-based behaviour from
0233b75 and references the bridge --session-id routing from f6a621d.
The /jira skill's Step 8 transitions the JIRA ticket to Done but didn't
touch Clay's sidebar, so after a clean /jira run the ticket showed Done
in Atlassian while the Clay session sat in Active. Add a third sub-step
that calls mark_session_done so the two endpoints stay in sync.

Mirrors what /done does on its own — the difference is that /jira
already has the issue key in context, so step ordering is "transition
JIRA, mark Clay" rather than the standalone /done's "find key,
transition JIRA, mark Clay".
When /jira launches in a freshly spawned session under plan mode at
high effort, the agent was diving straight into exploration and folding
the issue summary into its plan instead of printing the description
block to the user as Step 2 requires. The user lost the "what does this
ticket actually say" view they expect at the top of every spawn.

Tighten Step 2:
- Bold "Print the issue to the user before doing anything else."
- Provide a concrete markdown template (heading with the key + summary,
  Type/Priority/Status row, Description body, Acceptance Criteria) so
  there's no ambiguity about layout.

Add a matching Rules entry:
- Step 2's block is non-skippable, must print before exploration, must
  print even when the model thinks the user "already saw" the issue.

Both changes mirror to skills/jira.md and the local
~/.claude/commands/jira.md.
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