Interactive planning sessions launched with --permission-mode plan, which
gates EVERY MCP tool call regardless of --allowedTools (verified: even a
read-only mcp__claudedo__list_task_lists is denied under plan mode). So the
session prompted the user on the first CreateChildTask -- the whole point of
a planning session.
Switch BuildPlanningStartArgs/BuildPlanningResumeArgs to --permission-mode
default, which honours the allowlist. File edits stay blocked via the planning
system prompt + AllowedTools omitting Write/Edit/Bash. Resume also re-passes
--allowedTools, since the CLI does not restore it across --resume.
The earlier 'glob does not match' hypothesis was empirically falsified:
mcp__claudedo__*, the bare server name, and the explicit tool name all allow
the tool with zero permission_denials in default mode.
Planning start/resume opened an external Windows Terminal (wt) window.
Route them through the embedded ConPTY Command Center pane instead, matching
the existing interactive-session UX (no external window).
- Extract bare planning arg builders (BuildPlanningStartArgs/ResumeArgs) from
WindowsTerminalLauncher; the wt path still uses them (kept, not removed).
- InteractiveLaunchSpecService.BuildPlanningStart/Resume map a planning
context into a LaunchSpec (planning args + env: MAX_THINKING_TOKENS,
CLAUDEDO_PLANNING_TOKEN). Hub GetPlanningStart/ResumeLaunchSpec run
StartAsync/ResumeAsync then return the spec.
- UI: OpenPlanningSession + the resume branch raise OpenPlanningConPtyRequested;
the shell opens Mission Control and hosts a planning ConPTY pane.
Env is process-global by design (sequential human-paced sessions). wt planning
code retained. Tests added for the arg/env mapping.
- CancelAsync now signals the running Claude process of the cancelled task and
its cascaded children via the new RunCancellationRegistry (queue + override
slots register their CTS there) instead of only flipping DB state.
- external MCP review_task 'approve' now mirrors the hub's ApproveReview:
unit merge for parents, ApproveAndMergeAsync for childless tasks, optional
targetBranch; ReviewTaskResult carries mergeStatus/conflicts.
Nullable filter/patch params across the External/ MCP tool classes (ListTasks,
UpdateTask, AddSubtask, ReviewTask, SetMyDay, SetListConfig/SetTaskConfig,
CreateList/UpdateList) lacked C# default values, so the generated tool schema
marked them required — MCP clients omitting them (the common case) failed.
Gave every such parameter a default value.
Also registered a call-tool filter (ExternalMcpExceptionFilter) on the external
MCP host that translates InvalidOperationException/ArgumentException into
McpException, since the SDK's own catch-all discards ex.Message for any other
exception type and returns a generic "An error occurred invoking 'X'." string.
Added a reflection-based schema test sweeping every [McpServerToolType] class
to guard against reintroducing a required-but-nullable parameter.
HasChangesAsync counted untracked files, so a stray file in the shared
target working dir (e.g. left by a concurrent session) blocked merge
preflights even though nothing tracked changed. Add an includeUntracked
overload defaulting to true, and pass includeUntracked: false only from
the two target-working-tree merge preflights (TaskMergeService.MergeAsync,
PlanningMergeOrchestrator.StartAsync). Auto-commit and the worktree
cleanup data-loss guard keep counting untracked files, since those
callers need to know about them.
git add -A resolves the index U-stage regardless of file content, so a
conflicted file whose markers were never removed still got staged and
committed as-is. Check previously-conflicted files for leftover
<<<<<<< markers before staging; refuse with a conflict result if any
remain, leaving the repo mid-merge so Abort still cleans up.
Adds a regression test that leaves one conflict unresolved and asserts
ContinueMergeAsync refuses (no commit) and AbortMergeAsync still
restores a clean tree.
Confirmed via docs/open.md (Korrektheits-Review 2026-06-09): TaskStateService.CancelAsync
only flipped the target row to Cancelled - a Queued/Running child kept going and could
still commit into its worktree after the parent was gone.
CancelAsync now also cancels the task's own non-terminal children (Idle/Queued/Running/
WaitingForReview/WaitingForChildren) in the same pass, clearing BlockedByTaskId so no
successor is left wedged, and broadcasts TaskUpdated for each.
DeleteTask never re-evaluated a WaitingForChildren parent, so deleting
the last non-terminal child left it stuck (the BlockedByTaskId SET NULL
FK only repairs the blocked chain, not parent status). Expose
TaskStateService.TryAdvanceParentAsync on the interface and call it
from ExternalMcpService.DeleteTask after a child delete.
OnChildFinishedAsync ignored CancelAsync's result. If a chain successor
sat in a non-cancellable state (e.g. parked to Idle out of band) when
its predecessor failed/was cancelled, CancelAsync was a silent no-op and
the cascade stopped there, leaving the rest of the chain Queued+blocked
forever. Now it walks past any link CancelAsync can't touch and keeps
cancelling downstream.
The embedded ConPTY terminal replaced the in-app streaming interactive session, so
delete the dead stack: StreamingClaudeSession, InteractiveSessionService,
ProcessClaudeStreamTransport, IClaudeStreamTransport, ILiveSession, LiveSessionRegistry,
IdleSessionReaper (+ WorkerConfig.InteractiveIdleTimeoutMinutes), the WorkerHub
interactive methods + HubBroadcaster events, IWorkerClient interactive members, the
TaskMonitorViewModel composer + SessionTerminalView composer markup, and the old
'Run interactively' entry. AskUser/PendingQuestionRegistry, the autonomous path,
planning, ResumeTaskInTerminal, and all ConPTY code are kept. Localization pruned.
A fresh (non-resume) task-based ConPTY session now opens claude on the task's
prompt (title + description) as the positional argument, so the session starts on
the task instead of an empty prompt. Resume sessions and ad-hoc sessions are
unchanged.
BuildForTaskAsync now creates a worktree on demand (via WorktreeManager.CreateAsync,
the same path TaskRunner uses) when a task has a configured working dir but no
Active/Kept worktree, returning a fresh-start spec -- so never-run tasks can be
opened interactively. Adds BuildForDirectoryAsync + GetAdHocLaunchSpec hub/client
for ad-hoc sessions in an arbitrary directory (no task, no worktree, no skill seeding).
Adds InteractiveLaunchSpecService + GetInteractiveLaunchSpec hub method that
prepares a task worktree (session-skills seeding, run env) and returns a
LaunchSpec {cwd,exe,args,env} for a UI-hosted ConPTY terminal. Reuses
ISessionSkillSeeder, TaskRunner.UnionSkillNames, and WindowsTerminalLauncher
resume-arg/resolve logic. Guards mirror ResumeTaskInTerminal; a never-run task
yields a fresh-start spec instead of an error.
Interactive/streaming sessions are persistent claude.exe processes that
wait on stdin and never exit on their own. The only teardown was an
explicit StopInteractiveSession from the UI — there is no client-disconnect
or shutdown sweep — so an abandoned chat (UI closed, navigated away,
crashed) kept its claude.exe (+ conhost) alive for the worker's whole
lifetime. Under a long-running autostart worker these accumulate to dozens
of orphaned child processes.
LiveSessionRegistry now tracks per-session activity (Touch on every output
line and user action) and exposes ReapIdleAsync, which stops sessions idle
past a timeout while skipping any with a turn in flight. IdleSessionReaper
(BackgroundService) sweeps every 5 min; idle timeout defaults to 30 min,
configurable via interactive_idle_timeout_minutes (0 disables).
StreamingClaudeSession.RemoveQueuedAsync drops the first occurrence of a queued
message from _pending and re-broadcasts the updated queue. Wired through
InteractiveSessionService + WorkerHub.RemoveQueuedInteractiveMessage +
IWorkerClient.RemoveQueuedInteractiveMessageAsync. Removal by text (first match)
is robust to a turn flushing mid-click. Fakes + ILiveSession impls updated.
StreamingClaudeSession raises onQueueChanged (pending snapshot) and onUserMessageSent
(on delivery, incl. the seeded first prompt); InteractiveSessionService forwards these
as InteractiveQueueChanged/InteractiveMessageSent broadcasts. Lets the UI show queued
messages above the input and move a message into the transcript only when actually
delivered to Claude. Client events + fakes updated.
StreamingClaudeSession now buffers a mid-turn user message in a FIFO queue and
flushes one when the turn's result arrives (no implicit interrupt). InterruptAsync
only writes the control_request (no-op when idle); the resulting turn-end then
flushes any queued message. New InteractiveSessionService.InterruptAsync +
WorkerHub.InterruptInteractiveSession + IWorkerClient.InterruptInteractiveSessionAsync.
Adds SendInteractiveMessageAsync/StopInteractiveSessionAsync and the
InteractiveSessionStarted/Ended events to IWorkerClient + WorkerClient
(UI-thread dispatch mirroring TaskQuestionAsked). Updates the IWorkerClient
fakes in both test projects.
InteractiveSessionService resolves a task's list working dir + seeded prompt,
spawns a StreamingClaudeSession (claude stream-json in the list dir, model+auto
as before), registers it in LiveSessionRegistry, streams output over TaskMessage,
and broadcasts InteractiveSessionStarted/Ended (an exit watcher fires Ended). The
hub's OpenInteractiveTerminalAsync now starts this in-app session; SendInteractiveMessage
and StopInteractiveSession route to it. The external Windows-Terminal interactive
launch (LaunchInteractiveAsync / InteractiveLaunchContext / OpenInteractiveAsync) is
removed; planning sessions keep their terminal launch.
StreamingClaudeSession drives claude --input-format stream-json over a kept-
open stdin: sends user messages, interrupts the in-flight turn via the verified
control_request protocol, and tracks turn state from result events (treating an
interrupt-aborted error_during_execution result as turn-ended). IClaudeStreamTransport
abstracts the process I/O so it is unit-tested with a fake (no real claude).
LiveSessionRegistry maps taskId -> live session for the hub to route into.
Backs the upcoming in-app interactive sessions; autonomous task execution untouched.
TaskMonitorViewModel surfaces a pending AskUser question (TaskQuestionAsked /
TaskQuestionResolved events) with an AnswerDraft + SubmitAnswerCommand that calls
the new IWorkerClient.AnswerTaskQuestionAsync; MonitorPaneView shows an accent
question banner with an input box above the terminal. Pending question is cleared
on answer/resolve/finish and re-hydrated on attach via GetPendingQuestionAsync.
en/de localization for missionControl.question.*; test fakes updated.
A running task can call mcp__claudedo_run__AskUser(question) to block (up to 3
min) on a human answer. PendingQuestionRegistry holds the pending question +
TaskCompletionSource; the tool broadcasts TaskQuestionAsked, awaits the answer
(WorkerHub.AnswerTaskQuestion resolves it), and returns it as the tool result —
or a 'proceed on your judgment' fallback on timeout. The run stays Running
throughout (no status/schema change). ClaudeProcess raises MCP_TOOL_TIMEOUT so
the 60s HTTP-MCP cap doesn't kill the wait; the run MCP is now wired for every
task, not just standalone ones. System prompt updated to reconcile 'unattended'.
Add seven best-effort batch variants of the single-entity external MCP
tools: batch_get_tasks, batch_add_tasks, batch_update_task_status,
batch_cancel_tasks, batch_delete_tasks, batch_set_my_day, and
batch_cleanup_task_worktrees. Each loops the existing ExternalMcpService
methods sequentially (scoped DbContext is not thread-safe), returns a
per-item result array so a failing item never aborts the rest, and
rejects empty or over-100-item batches. Merge/review stay single-task.
wt.exe treats ';' as a command/tab delimiter in every argument, with no escape
that survives quoting (microsoft/terminal#13264), so a task description
containing ';' spawned extra terminals on "Run interactively" and planning start.
Route the launch as wt -> powershell -> claude and pass the free-text prompt via
$env:CLAUDEDO_LAUNCH_PROMPT so it never reaches the wt command line; PowerShell
binds the variable as a single argument (embedded quotes escaped for PS 5.1).
Also clarify the launcher, which serves interactive runs too (not just planning):
IPlanningTerminalLauncher -> ITerminalLauncher, WindowsTerminalPlanningLauncher ->
WindowsTerminalLauncher, LaunchStart/Resume -> LaunchPlanning{Start,Resume}Async.
OnlineSyncService is registered once at startup; toggling the feature off
in Settings persisted the flag but never stopped the running loop, so it
kept polling and failing OIDC discovery every cycle. Guard TickAsync on
the shared config's Enabled flag so disabling takes effect live.
AttachmentMcpTools exposes add_task_attachment (text or base64),
list_task_attachments, and remove_task_attachment on the external MCP
endpoint, so an agent can prepare reference files (plans, scripts) on a task
that will run later. Re-attaching the same name overwrites; add/remove refuse
on a running task.
TaskRunner appends attached files (absolute paths) to the run prompt as the
read-only Reference files section. Task and list deletes now remove the
on-disk attachment dir eagerly, and a startup AttachmentOrphanRecovery sweep
drops any attachments/<taskId>/ whose task no longer exists (covers list
cascade and planning-discard paths).
GetConflictsAsync/GetMergeConflicts (+ MergeConflicts/ConflictFileContent/ConflictFileDto/ConflictHunkDto DTOs and the now-orphaned GitService.ShowStageAsync) were superseded by the segment-based GetMergeConflictDocuments path and had no production callers. Removes the IWorkerClient member, both test fakes, the lingering test, and updates the Worker/Ui/Data CLAUDE.md surface notes.
Replace the whole-file conflict model with line-level hunks, the
foundation for the full in-app merge editor.
- ConflictMarkerParser: parses git conflict markers (incl. diff3 base)
into ordered stable/conflict MergeSegments; exact round-trip + Compose
- GitService.MergeNoFfAsync passes -c merge.conflictStyle=diff3 so the
working tree carries the merge base in conflict markers
- TaskMergeService.GetConflictDocumentsAsync: reads each conflicted file,
parses into segments, flags binary files
- hub GetMergeConflictDocuments + DTOs (MergeConflictDocumentsDto/
ConflictDocumentDto/MergeSegmentDto), IWorkerClient + both fakes
- tests: 8 parser unit tests + a real-git integration test asserting
line-level hunks with a diff3 base
ZitadelAuthProvider cached the access token in memory and only re-read the
refresh token when the cache expired. Re-signing as a different user saved a
new refresh token but the worker kept serving the previous user's cached
access token until it expired — so sync (and ownerId stamping) continued under
the old identity.
Track the refresh token that minted the cached token and invalidate the cache
when the stored refresh token changes (user switch or sign-out). Switching
users now takes effect on the next sync without a worker restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plumb a per-resource owner (Zitadel sub) through the sync contract without
enforcing isolation client-side — the server stays the authority.
- Dtos: add optional ownerId to RemoteList/RemoteTask/MirrorTask
- JwtClaims: decode the sub claim from the access token (never throws)
- OnlineSyncService: stamp ownerId on pushed lists + mirror; defensively skip
pulled tasks owned by a different user (unowned tasks still sync, so
single-user behavior is unchanged)
- docs: contract documents ownerId + multi-user readiness
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Online API now requires the "user" project role (claim
urn:zitadel:iam:org:project:roles) instead of an ALLOWED_USER_IDS allowlist.
- IOnlineAuthProvider: add GetAccessTokenAsync(forceRefresh) overload
- ZitadelAuthProvider: forceRefresh drops the cached token and re-runs the
refresh-token grant to mint a fresh, role-bearing token
- OnlineInboxApiClient: on 401, force-refresh and retry once; if still 401,
throw a clear "missing 'user' role" error
- OnlineSyncService: surface the 401 at Error level (no longer silent)
- UI: ZitadelTokenInspector decodes the access token after login and warns
early when the "user" role is absent (fail-open); shown in settings
- docs: online-inbox-api-contract reflects role-based access (no allowlist)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hub: GetOnlineInboxState / SetOnlineInboxConfig / SetOnlineInboxAuth /
ClearOnlineInboxAuth. WorkerConfig.SaveOnlineInbox persists only the
online_inbox section. OnlineTokenStore + config registered always so hub
methods work when sync is disabled. IWorkerClient surface + all test fakes
synced. RedirectUri config (default http://localhost:8765/callback).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The API base URL is https://claudedo.kuns.dev/api — leading-slash request
paths discarded the /api segment. Use relative paths so they nest under the
base. Tests now use a /api/ base to guard the regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Headless refresh-token -> access-token exchange via OIDC discovery + token
endpoint. Cached to expiry (60s margin), thread-safe, persists rotated refresh
tokens, graceful null on invalid_grant/network errors. Wired into DI when
online_inbox is enabled. Interactive PKCE login (UI) still pending the
registered redirect URI. 7 tests, stubbed HttpMessageHandler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>