Skip to content

Add 'Ask agent to fix' for CI failures - #8

Merged
centdix merged 6 commits into
mainfrom
quickfixbutton
Feb 25, 2026
Merged

Add 'Ask agent to fix' for CI failures#8
centdix merged 6 commits into
mainfrom
quickfixbutton

Conversation

@centdix

@centdix centdix commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an "Ask agent to fix" button in the CI details dialog that sends failing CI logs to the agent
  • Fixes sendPrompt truncation by replacing chunked tmux send-keys -l with load-buffer/paste-buffer (chunks starting with - were parsed as tmux flags)
  • Preamble is typed via send-keys (visible inline), logs are pasted via buffer (collapsed as [pasted text])
  • CI logs endpoint now tries --log first with --log-failed fallback

Test plan

  • Open a worktree with a failing CI check
  • Click "View logs" then "Ask agent to fix"
  • Confirm no send-keys errors in backend terminal
  • Confirm the preamble shows inline and logs appear as [pasted text] in the agent

🤖 Generated with Claude Code

Replace chunked tmux send-keys with load-buffer/paste-buffer to avoid
flag interpretation errors (e.g. chunks starting with "-" parsed as
tmux flags). The preamble is typed via send-keys so it shows inline,
while logs are pasted via buffer (collapsed as [pasted text]).

Also adds --log with --log-failed fallback for the CI logs endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Feb 25, 2026

Copy link
Copy Markdown

Claude finished @centdix's task in 1m 36s —— View job


Code Review: Add "Ask agent to fix" for CI failures

  • Read repository setup instructions (CLAUDE.md)
  • Analyze PR diff and changed files
  • Review backend changes (server.ts, workmux.ts)
  • Review frontend changes (App.svelte, AgentStatusIcon.svelte, CiDetailsDialog.svelte, api.ts)
  • Post review findings

Overall this is a well-structured PR. The load-buffer/paste-buffer approach is a solid fix for the send-keys truncation issue, and the preamble/payload split is a nice UX touch. A few issues worth addressing:

Bug

Dropped "done" icon in AgentStatusIcon.svelte — The formatter refactor accidentally removed the {:else if status === "done"} branch that rendered a green checkmark SVG. The pillClass function still handles "done" (returns success colors), but no icon will render when status === "done". This looks like an unintentional deletion during the formatting cleanup.

Fix this →

Potential Issues

Race condition on shared tmux buffer name (workmux.ts:355) — The buffer name wm-prompt is hardcoded. If two concurrent sendPrompt calls happen (e.g., two different worktrees), the second load-buffer could overwrite the first's buffer before paste-buffer runs. Consider using a unique buffer name per invocation (e.g., wm-prompt-${branch} or a random suffix).

No log size limit before sending to agent (CiDetailsDialog.svelte:78) — normalizeLogsForPrompt strips ANSI codes but doesn't truncate. CI logs can be very large (especially verbose test output). Sending megabytes of text to a tmux buffer could overwhelm the agent or hit paste limits. Consider truncating to the last N lines or capping at a max character count with an indicator like [... truncated].

--log fetches all job logs, not just failures (server.ts:309) — Switching to --log as the primary option returns logs for all jobs in a workflow run, not just failing ones. For workflows with many passing jobs, this could return significantly more data than needed. Consider keeping --log-failed as the primary and using --log as the fallback, or adding the --failed filter.

Nits

  • CiDetailsDialog.svelte:90-91 — Missing blank line between handleFix and handleCopy functions (minor style inconsistency).
  • findWorktreeSession (workmux.ts:391) — line.split(":") assumes neither the session name nor window name contain colons. This is fine in practice since wm-* names won't have colons, but a targeted split (e.g., line.split(":", 2) or indexOf) would be more defensive.

What looks good

  • The load-buffer/paste-buffer approach is the correct fix — avoids all the send-keys escaping issues with large payloads.
  • Good defensive measures: null byte stripping, -- separator for preamble send-keys, individual error handling per tmux operation.
  • The ANSI stripping regex handles both CSI and Fe escape sequences well.
  • Clean separation of preamble (visible inline) vs. logs (pasted as buffer) is good UX.
  • findWorktreeSession properly decouples session lookup from the hardcoded wm-* convention.

@centdix

centdix commented Feb 25, 2026

Copy link
Copy Markdown
Contributor Author

@claude fix the issues, one commit per fix
for max log size use const = 30000 char
use --log-failed always, not --log

@claude

claude Bot commented Feb 25, 2026

Copy link
Copy Markdown

Claude finished @centdix's task in 2m 37s —— View job


Fixed review issues — 5 commits pushed

  • Read CLAUDE.md and relevant files
  • Fix 1: Restore dropped "done" icon in AgentStatusIcon.svelte
  • Fix 2: Fix race condition with hardcoded tmux buffer name in workmux.ts
  • Fix 3: Add 30000 char log size limit in CiDetailsDialog.svelte
  • Fix 4: Use --log-failed always in server.ts
  • Fix 5: Nits (blank line, defensive split)

Summary of changes (one commit each):

Commit Change
0b37a18 Restore "done" checkmark SVG in AgentStatusIcon — was missing after formatter refactor
a6981da Unique tmux buffer name per sendPrompt call (wm-prompt-<timestamp>-<random>) to prevent concurrent overwrites
2cb7faf Cap CI logs at MAX_LOG_CHARS = 30000, keeping the last 30k chars with [... truncated] prefix
422ac70 Use --log-failed exclusively (remove --log primary + fallback logic)
57a332e Blank line between handleFix/handleCopy; use indexOf(":") in findWorktreeSession for correctness

github-actions Bot and others added 5 commits February 25, 2026 18:09
The {:else if status === "done"} branch was accidentally removed during
the formatter refactor, leaving no icon rendered for completed agents.
Add back the green checkmark SVG between the "waiting" and "error" branches.

Co-authored-by: centdix <centdix@users.noreply.github.com>
Hardcoded "wm-prompt" buffer could be overwritten by a concurrent
sendPrompt call before paste-buffer runs. Use a timestamp+random suffix
(e.g. wm-prompt-1234567890-abc12) to avoid the race.

Co-authored-by: centdix <centdix@users.noreply.github.com>
Large CI outputs (verbose test suites, etc.) could overwhelm the tmux
paste buffer or the agent context. Truncate from the start, keeping the
last 30000 chars (most relevant tail), with a [... truncated] marker.

Co-authored-by: centdix <centdix@users.noreply.github.com>
--log returns logs for all jobs (including passing ones), producing far
more data than needed. Use --log-failed exclusively to stay focused on
failing job output.

Co-authored-by: centdix <centdix@users.noreply.github.com>
- Add missing blank line between handleFix and handleCopy
- Use indexOf(":") in findWorktreeSession so session/window names
  containing colons don't silently mis-parse

Co-authored-by: centdix <centdix@users.noreply.github.com>
@centdix
centdix merged commit f90573b into main Feb 25, 2026
1 check passed
@centdix
centdix deleted the quickfixbutton branch March 1, 2026 15:27
hugocasa added a commit that referenced this pull request May 13, 2026
…close

M1: terminal WebSocket input/sendKeys now disarms the oneshot watcher on the
first message per connection so the human taking over via the terminal pane
doesn't get steamrolled by the auto-close.

M2: re-read meta immediately before closeWorktree so a browser-side disarm
that lands during the postToLinear HTTP call aborts the close.

M3: README documents the `_oneshot` label trigger and its precedence over the
`webmux` label.

L1: switch the last inline seed-from-Linear deps call site to the shared
defaultSeedFromLinearDeps helper.

L2: distinguish watcher-driven disarm from a real user takeover in CLI logs
by checking whether mux is still alive at the disarm transition.

L3: lifecycle endpoints (open without re-arm, close, archive, merge) call
disarmOneshotIfArmed so end-of-session actions also clear armed meta.

L4: treat agentLifecycle === "closed" as terminal in the watcher so an
externally killed agent doesn't leave meta armed indefinitely.

L6: expose `oneshot` on the frontend WorktreeInfo and `mapWorktree` so the
UI has access to the armed state.

L7: oneshot-watcher-service tests now cover postToLinear failure, disarm
during postToLinear, closeWorktree throwing, and the `closed` lifecycle path.

L8: source-level regression test asserts every disarm reason string the
watcher relies on is actually wired in server.ts.

L9: tighten WebmuxConversationAttachmentPayloadSchema.agent to AgentIdSchema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
centdix pushed a commit that referenced this pull request May 18, 2026
* feat: add oneshot command and per-worktree on-merge action

Adds `webmux oneshot` for start-to-finish runs that stream the agent
conversation to stdout without changing tmux focus, plus per-worktree
`onMergeAction` ("close" | "remove" | null) configurable via CLI flags
(`--close-on-merge` / `--remove-on-merge`) and a frontend toggle in the
worktree row menu. `oneshot --resume <branch>` re-attaches to a stuck
session and can send a follow-up prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: linear handoff — post conversation + resume from issue

Adds a Linear-backed handoff path so a webmux session can be picked up
on another machine (or by someone else):

- oneshot --post-to-linear <issue-id|team-key>: when the run ends,
  upload the conversation as a JSON attachment + summary comment. Team
  keys auto-create a new issue first.
- oneshot/add --resume-from-linear <issue-id>: resolve a branch and
  conversation context from the issue's webmux attachment (preferred)
  or its linked GitHub PR, then create the worktree. --branch overrides
  the resolved branch.
- New CLI: webmux linear post <branch> <issue-or-team>.
- Frontend: "Post conversation to Linear…" worktree action, plus
  "Resume from Linear issue" field in the create dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: rename --resume-from-linear to --from-linear, always load issue body

The flag handles both fresh-issue kickoffs and resumes, so the "resume"
prefix was misleading. Renames everywhere and updates the seed builder
to always pull the issue title + description into the conversation
context, regardless of whether a webmux attachment or linked PR exists.

Also:
- New --linear <id> shorthand for --from-linear ID --post-to-linear ID
  (round-trip on the same issue, the common pattern)
- Conversation context now always includes a Linear hint reminding the
  agent to reference "Fixes ENG-X" in PR title/body so Linear auto-links
  the PR if the branch doesn't match issue.branchName
- Contract field resumeFromLinear → fromLinear

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: restrict --post-to-linear to team keys

Now that --linear handles the "round-trip on an existing issue" case,
posting an arbitrary session into a pre-existing issue is redundant and
muddles the mental model. Tighten the surface so:

- --post-to-linear TEAM — creates a new Linear issue in TEAM, posts there
- --linear ENG-X — loads the issue body as context AND posts back to it
- --from-linear ENG-X — loads only, no post

`webmux linear post <branch> <team-key>` and the dashboard Post-to-Linear
dialog get the same restriction. Issue-id input produces a helpful error
that points at --linear.

Also simplifies the --linear conflict logic — mixing --linear with
--from-linear or --post-to-linear is now always rejected (no more "same
id is ok" exception).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: oneshot exposes one --linear flag with auto-detect

Collapses --from-linear and --post-to-linear on oneshot into a single
--linear <ID|TEAM> that picks the right behavior from the input shape:

- ENG-123 (issue id) → load context from the issue + post results back
  (the round-trip case)
- ENG (team key) → no seed, create a new issue in the team when done

--from-linear stays on `webmux add` (no post lifecycle there), and the
manual `webmux linear post <branch> <team-key>` primitive is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: stream Claude conversations in oneshot via history polling

The /ws/agents/worktrees/:name handler short-circuits for non-Codex
providers (server.ts:733), so Claude oneshots got the initial snapshot
and then sat silent while the tmux agent was happily running. The
dashboard works around this with history polling alongside the WS; do
the same in oneshot so the conversation actually streams to stdout.

Dedup in printNewMessages makes this a safe additive change for Codex
too (acts as a fallback for missed deltas).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: silence Claude session JSONL parse-warning spam

The parse-fail warning fired on every history read for any line that
survived trim() but wasn't valid JSON — and with the new oneshot history
polling that meant ~30 warnings per minute. The line content was
typically empty-looking control characters that render blank.

- Add startsWith('{') as a cheap pre-filter so obvious non-JSON garbage
  doesn't reach JSON.parse.
- Downgrade the remaining log to debug so legitimate parse failures
  (e.g. mid-write partial JSON) don't spam stderr in normal use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: tag oneshot worktrees with a source badge in the UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: harden oneshot — autonomous prompt, tool-call visibility, idle exit, linear upload

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: pre-create linear issue for oneshot team-key target

So the agent's PR has the issue id in its branch name and Linear's GitHub
integration auto-links it. The team-key path now collapses into the same
round-trip flow as `--linear ENG-123`. Also makes oneshot tool-call output
compact (Claude-Code-style `● Tool(arg)` / `  ⎿ result`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: oneshot auto-closes worktree session on exit unless --keep-open

`--keep-open` now means "leave the worktree session running after oneshot
exits" — oneshot always exits on idle in both modes. Drops the implicit
`--close-on-merge` default so on-merge behavior matches `webmux add` (no
default; respects global autoRemoveOnMerge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert: drop per-worktree on-merge action — keep only the global toggle

Removes the auto-close service, the per-worktree `onMergeAction` override,
its CLI flags (`--close-on-merge`/`--remove-on-merge` on add+oneshot), the
UI dropdown, and the related API endpoint. The pre-existing global
`autoRemoveOnMerge` still gates auto-removal after PR sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: route LinearPanel click through the fromLinear seed

Drops the redundant "From Linear issue (optional)" text input on the
create dialog. The bottom-left LinearPanel was already the canonical
"start from Linear" entry point; now its click path sends
`fromLinear: { issueId }` so the backend injects the issue header and
any prior webmux attachment as context — same plumbing as the CLI's
`--linear ENG-123`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: drop stale onMergeAction refs from test fixtures + dedupe import

CI typecheck caught these after the revert + main-merge: test object
literals were still spreading `onMergeAction: null` / passing
`onsetonmergeaction` props, and the merge accidentally introduced a
duplicate `writeWorktreeMeta` import in lifecycle-service.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: address PR review — env-source unification, DRY, cleanups

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: address PR review #2 — preflight, drop dead Linear endpoints

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: stale flag-name strings + WebSocket reconnect ceiling

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: post-to-linear skips dialog when worktree is linked to an issue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: address PR review #5 — reconnect tuning, Zod validation, DRY

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: address PR review #6 — Zod-derived type, confirm guard, parser narrowing

- Derive WebmuxConversationAttachmentPayload from the Zod schema so the
  validated boundary and the type can't drift; remove the duplicate interface.
- Gate the linked-worktree Post-to-Linear menu action behind a ConfirmDialog
  ("Post conversation to ENG-N?") — the endpoint is non-idempotent and a
  misclick previously created a duplicate attachment + comment.
- Update LinearPostDialog's inline "looks like an issue id" hint to point at
  the new worktree-menu shortcut instead of the CLI fallback.
- Add a second WS reconnect warning at attempt 15 so the user has signal
  during the silent window between the first warn (~6s) and the fatal (~60s).
- Reject `--branch` without `--linear` in `parseOneshotArgs` (matches the help
  text); the positional branch covers the non-linear path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: address PR review #7 — Post-to-Linear in-flight guard, parser polish

- Track an in-flight `postingLinearBranches` set so the row menu's
  "Post conversation to ENG-N" item disables and shows "Posting to
  Linear…" while a request is pending; prevents a second click from
  firing a duplicate non-idempotent post before the first toast lands.
- Snapshot the linked issue id at click time (`postToLinkedConfirm =
  { branch, issueId }`) so the ConfirmDialog can't render "Post
  conversation to ?" if the worktree refreshes away between click
  and confirm.
- Drop the unnecessary `async` from `handlePostToLinear` — it only
  sets state synchronously now.
- Reorder `parseOneshotArgs` checks: `--branch` with `--resume` now
  surfaces a precise message ("Cannot use --branch with --resume")
  instead of the misleading "--branch only applies with --linear".
- Tighten `RECONNECT_WARN_AT` to a `readonly [number, number]` tuple
  for honest intent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: cover Post-to-Linear confirm flow and in-flight guard in App.test.ts

Two cases protecting `handlePostToLinear` / `confirmPostToLinkedIssue`:
- Linked worktree → menu item click opens the ConfirmDialog and "Post"
  fires `postWorktreeToLinear` exactly once with
  `{ kind: "issue", issueId: <identifier> }`. No `LinearPostDialog`
  rendered for linked worktrees.
- In-flight guard → while the post promise is unresolved, the row
  menu item disables to "Posting to Linear…" and a second click is
  a no-op (`postWorktreeToLinear` call count stays at 1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: send empty body when reopening a worktree from the UI

`openWorktree`'s contract body was switched from `c.noBody()` to
`OpenWorktreeRequestSchema` (with `prompt?: string`) earlier in this
branch, but `App.svelte` still called `api.openWorktree({ params })`
with no `body`. ts-rest sent no JSON, the server's `req.json()`
threw, and the user got `Invalid JSON` on every reopen.

Pass `body: {}` to match the CLI call site at oneshot.ts:733-736.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat: linear auto-handler — _oneshot label triggers a oneshot run

Adds a second variant to the Linear label-driven auto-handler. Same
poller and master toggle as the existing `webmux` flow; the label
chooses the action:

- `webmux` (existing) → create a worktree via lifecycleService.
- `_oneshot` (new) → spawn `webmux oneshot --linear ENG-X` as a
  detached child process. Output is dropped — oneshot posts results
  back to Linear on exit, so the user sees them there.

An issue tagged with both labels routes to oneshot (more autonomous
of the two). The dispatch is gated on a new `WEBMUX_CLI_ENTRY` env
var set by `webmux serve` — if the backend is launched standalone
without it, the `_oneshot` path is silently disabled.

- backend/src/services/linear-auto-create-service.ts: split filters
  into webmux-create vs oneshot, accept an optional
  `runOneshotForIssue` dependency, dispatch both per poll cycle,
  reuse the single `processedIssueIds` dedupe set.
- backend/src/server.ts: build `runOneshotForIssue` from
  `WEBMUX_CLI_ENTRY`, spawning a detached process pinned to the
  server's port. `proc.unref()` so it doesn't keep the server alive.
- bin/src/webmux.ts: export `WEBMUX_CLI_ENTRY` to the backend's env.
- backend/src/__tests__/linear-auto-create-service.test.ts: cover
  Todo-state + label gating, both-label routing, existing-worktree
  exclusion, case-insensitive matching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: move oneshot auto-close + Linear post-back to server

Moves the "1, 2, 5" responsibilities of oneshot mode (system prompt
injection — already server-side — plus auto-close on agent-done and
Linear post-back) out of the CLI driver and into a server-side
watcher. The CLI now just arms the watcher on create/open and owns
streaming + exit polling for the human watching the terminal.

Effect: a oneshot run can finish safely even if the CLI is killed,
the network drops, or the run was launched by the `_oneshot` Linear
label (no CLI to begin with). Any browser-originated interaction
with the session disarms the watcher so the human can take over.

- contract: `OneshotConfigSchema` carrying `autoCloseOnDone` and
  optional `postToLinearOnDone`. Plumbed through CreateWorktree and
  OpenWorktree request bodies.
- persisted meta: `WorktreeMeta.oneshot` lives on disk; presence is
  the "armed" signal.
- watcher: new `oneshot-watcher-service.ts` polls `source: "oneshot"`
  worktrees every 3s; after a 15s idle-grace (or immediately on
  stopped/error), fires `postToLinear` then `closeWorktree` and
  disarms via `lifecycleService.disarmOneshot`.
- disarm hooks: `apiSendPrompt`, `apiSendAgentsWorktreeMessage`,
  `apiInterruptAgentsWorktree`, and `apiUploadFiles` all clear the
  oneshot meta. First browser action wins — no race, no duplicate
  Linear post.
- CLI trim: drop the in-CLI close + post-back loops from
  `runOneshot`. Send `oneshot: {...}` in the create/open body so the
  server takes over. Resume path threads the same config through
  `openWorktree`.
- label trigger: `runOneshotForIssue` now calls
  `lifecycleService.createWorktree({ source: "oneshot", oneshot: {
  autoCloseOnDone: true, postToLinearOnDone: ... } })` directly
  instead of spawning the CLI. `WEBMUX_CLI_ENTRY` plumbing is gone.
- shared helper: `postWorktreeConversationToLinear` lifted out of
  `apiPostWorktreeToLinear` so the watcher can call it without HTTP.
- tests: 8 watcher cases covering idle-grace, immediate terminal,
  post-then-close ordering, disarmed-meta skip, autoClose=false,
  isActive gate, non-oneshot source skip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: address self-review before reopening PR

Fixes the high-severity items the post-merge review surfaced:

- **Watcher silently disabled in most cases.** `startOneshotWatcher`
  was gated on `hasRecentDashboardActivity`, which is zero for a
  CLI-only or label-triggered run. Drop the gate; iterating the
  worktree list every 3s is cheap when no oneshot worktrees exist.
- **CLI misclassified user-takeover as failure.** Expose `oneshot`
  on `ProjectWorktreeSnapshot` (server-side path: meta → reconciled
  state → runtime → snapshot) so the CLI's polling FSM can detect
  the watcher's disarm transition. New `onUserTookOver` callback in
  `pollProjectState` exits cleanly with code 0 and a "user took
  over" message instead of "agent idle without opening a PR" (1).
- **`disarmOneshotIfArmed` now updates runtime state** via the new
  `ProjectRuntime.setOneshot`, so the next snapshot reflects the
  disarm without waiting for a reconcile pass. Log level bumped from
  debug to warn on failure.

Plus the easy mediums:

- Extract `normalizeOneshotConfig(input)` helper (was duplicated in
  `apiCreateWorktree` and `apiOpenWorktree`).
- Gate `pollConversationHistory` on Claude — Codex has live deltas
  via WS, polling there was wasted load.
- `OneshotPostTarget` now re-exports `PostWorktreeToLinearTarget`
  from the contract instead of declaring a near-identical type.
- Update `getOneshotUsage` to reflect server-driven close + post
  and the disarm-on-interaction semantics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: dedupe linear auto-create filters + seed-from-Linear deps

- linear-auto-create-service: collapse `filterAutoCreateIssues` and
  `filterAutoOneshotIssues` onto a shared `filterTriggerableIssues`
  helper that owns the Todo + processedIssueIds + existing-branch
  logic. Each public filter now only specifies its label predicate.
- conversation-export-service: export `defaultSeedFromLinearDeps`
  bundling `fetchIssueWithAttachments` + `downloadWebmuxAttachment`.
  Replaces the inline 3-line record at all three call sites
  (server.runOneshotForIssue, server.apiCreateWorktree.fromLinear,
  CLI's --linear path). Test stubs keep using their own deps object.

No behavior change. 396 backend + bin tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: address PR review #8 — disarm on terminal input, race-safe close

M1: terminal WebSocket input/sendKeys now disarms the oneshot watcher on the
first message per connection so the human taking over via the terminal pane
doesn't get steamrolled by the auto-close.

M2: re-read meta immediately before closeWorktree so a browser-side disarm
that lands during the postToLinear HTTP call aborts the close.

M3: README documents the `_oneshot` label trigger and its precedence over the
`webmux` label.

L1: switch the last inline seed-from-Linear deps call site to the shared
defaultSeedFromLinearDeps helper.

L2: distinguish watcher-driven disarm from a real user takeover in CLI logs
by checking whether mux is still alive at the disarm transition.

L3: lifecycle endpoints (open without re-arm, close, archive, merge) call
disarmOneshotIfArmed so end-of-session actions also clear armed meta.

L4: treat agentLifecycle === "closed" as terminal in the watcher so an
externally killed agent doesn't leave meta armed indefinitely.

L6: expose `oneshot` on the frontend WorktreeInfo and `mapWorktree` so the
UI has access to the armed state.

L7: oneshot-watcher-service tests now cover postToLinear failure, disarm
during postToLinear, closeWorktree throwing, and the `closed` lifecycle path.

L8: source-level regression test asserts every disarm reason string the
watcher relies on is actually wired in server.ts.

L9: tighten WebmuxConversationAttachmentPayloadSchema.agent to AgentIdSchema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: address PR #238 review — label dedup eviction, runtime-state disarm

M1: auto-create service evicts processedIssueIds entries whose issue is no
longer Todo+labeled, so removing and re-adding the label actually retriggers
(matching the README). Tested with the new "re-creates after the label is
removed and re-added" case.

M2: terminal WS input/sendKeys check projectRuntime.getWorktreeByBranch().oneshot
(in-memory, cheap) instead of a per-connection cache. A re-arm on the same WS
is now detected without a stale-cache miss.

L1: new lifecycle-service test exercises disarmOneshot end-to-end on real
on-disk meta — arms, disarms, asserts the oneshot block is gone but profile,
allocatedPorts, startupEnvValues, branch are preserved. Also asserts idempotency.

L2: oneshot badge tooltip is now "Autonomous run — auto-closes when done"
to cover both the `oneshot` CLI and the `_oneshot` Linear label trigger.

L3: lifecycle-service.disarmOneshot uses a plain `delete` on a shallow copy
instead of the awkward destructure-and-`void` pattern.

L4: OneshotWatcherDependencies.lifecycleService narrowed to
`Pick<LifecycleService, "closeWorktree" | "disarmOneshot">` so watcher tests
can mock without `as unknown as LifecycleService`.

L5: comment in apiCreateWorktree's fromLinear fallback clarifying it only
fires for dashboard/REST callers (CLI resolves the seed in-process first).

NIT: JSON.parse failure in parseClaudeSessionRecords logs at `warn` again so
a corrupt session file is visible — the prefix filter at `debug` still drops
non-JSON noise silently.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: cold-start race + runtime mirror in oneshot watcher

M1: the previous "closed = terminal" patch was wrong — `closed` is also the
default lifecycle for a freshly upserted worktree before the agent's first
event arrives. Treat it like `idle` (needs the 15s idle grace) so a cold-start
session that resolves to running/idle within the grace window survives, while
a genuine post-run close still fires after the grace expires. Replaces the
"closed = immediate fire" test with two cases that cover both behaviors.

L1: mirror the watcher's `disarmOneshot` to `ProjectRuntime` so the snapshot
reflects the cleared armed state immediately. Without this, when
`autoCloseOnDone=false` no close-driven reconcile fires and
`snapshot.oneshot` stays armed in memory, defeating the CLI's
"user took over" detector.

L2: `postWorktreeConversationToLinear` now passes `pkg.version` as
`webmuxVersion` so Linear summary comments include the producing CLI version.

L3: `forcePrSync` in the CLI poller now logs a warning to stderr on failure
instead of swallowing — a network blip during the post-stable sync was
indistinguishable from "agent stuck without opening a PR".

L4: `ensureWorktreeReady` now waits for status to leave `closed`, not just
`creating`. Pairs with M1 — the CLI no longer starts polling during the
cold-start window the watcher is now correctly ignoring.

NIT: comment on `consecutiveClosedReadings >= 2` explains the ~3-6s lag is a
deliberate trade-off against false-positive close events on reconcile gaps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: rename Linear oneshot trigger label `_oneshot` → `webmux_oneshot`

A bare `_oneshot` is too generic and easy to collide with unrelated team
conventions; prefixing with `webmux_` makes ownership obvious at a glance and
matches the `webmux` companion label.

No behavior changes — same case-insensitive match, same precedence over the
plain `webmux` label.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: don't disarm oneshot watcher when reopening a closed session

Opening a closed worktree (the dashboard's "Enter" / Open button on a stopped
session) is "let me peek at the agent's progress", not "I'm taking over". The
disarm should fire only on actual interaction — terminal input, chat send,
upload, etc. — which already happen via their respective endpoints.

Drops the `open-worktree` disarm + the corresponding wiring-test entry. Close,
archive, and merge still disarm (those are decisive end-of-run actions).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: --resume requires --prompt + dedupe permanent linear-auto-create failures

L2: `webmux oneshot --resume <branch>` without `--prompt` was silently broken
for Claude — `claude --continue` doesn't fire UserPromptSubmit, so lifecycle
stays "closed" and the CLI times out at 60s with exit 1. Codex worked because
`codex resume --last` fires SessionStart unconditionally. The parser now
rejects this combination with a clear error pointing at the dashboard for
re-attach-without-prompting.

L3: linear-auto-create now adds the issue id to `processedIssueIds` on
failure as well as success. Without this, a permanent error (e.g. "Branch
already exists" for an out-of-band local branch) would retry every 60s
forever. The label-eviction pass still lets the user retrigger by removing
and re-adding the label after fixing the underlying issue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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