feat: Claude Code plugin compatibility + subagent hardening + bump v1.1.26 - #77
Merged
Merged
Conversation
…ult re-injection - PermissionSnapshot + snapshot()/from_snapshot() for Claude-style inheritance - spawn_subagent inherits parent mode/allow-list/settings (fixes Plan read-only escape) - run_in_background/max_turns/resume input fields + async_launched handle - pending_subagent_results queue drained into <subagent-finished> before next LLM call - SubagentFinished + SubagentFinishedNotification protocol events + driver wake-up - subagent_runs SQLite table + SubagentManager with orphan repair - check_subagent tool; input-aware keep_live; transcript carry-over in TUI - concurrent permission-select queue; docs + bilingual Ch26 issue log
- spawn_subagent gains worktree: true — child runs in a git worktree lane (subagent-<child_id>, branch wt/subagent-<child_id>) with work_dir pointed at the lane; created synchronously, reused on resume, kept after completion - per-invocation resource resolution: worktree-isolated spawns map to ToolResources::independent() so they can fan out in the same wave as other tools without racing the main tree; plain spawns stay ResourcePolicy::Barrier - WorktreeManager::get made public (used by the subagent handler) - summary now appends (worktree: <name> at <path>) for isolated runs - TUI: mouse-wheel over the slash-command / select popup steps the selection instead of scrolling the log behind it; popup hit areas cleared per frame - docs: Ch 12 subagent bilingual + Ch 26 issue log bilingual, implementation plan docs/superpowers/plans/2026-08-27-subagent-worktree-isolation.md
…mands/agents/hooks/mcp)
…nds/*.md as slash commands - validate_plugin_candidate now accepts any supported feature (skills, commands, agents, hooks, mcp) instead of hard-requiring skills/ — unblocks 15+ official marketplace plugins (LSP docs, commit-commands, code-review…) - full plugin.json manifest parse (description/version/author/hooks/mcpServers) - InstalledPlugin records command_count/agent_count/has_hooks/has_mcp (serde-defaulted, old installed.json records stay valid) - tact plugin list shows a feature summary per plugin - SkillRegistry::load_plugin_commands loads commands/*.md as plugin:<name> skills (Claude: same as skills, command wins over same-named skill) - SkillFrontmatter parses argument-hint / allowed-tools / model - PluginStore::installed_plugin_roots generic root helper
…mcpServers) - McpProjectConfig parses Claude .mcp.json entries (stdio/http); http/url transports are skipped with a warning (client is stdio-only) - installed_plugin_mcp_servers scans every installed plugin root for .claude-plugin/plugin.json mcpServers and .mcp.json, naming servers plugin__<id>__<server> (same prefix scheme as the cwd PluginLoader) - load_mcp_router merges installed-plugin servers after the cwd scan
…agents/*.md) - new agent_def module: SubagentDefinition registry loading project-local .tact/agents/*.md (plain names) and installed plugin agents/*.md (plugin:<name>); frontmatter tools/model/permissionMode parsed - ToolContext carries SharedAgentDefinitionRegistry (built in interactive/ headless like skill_registry) - spawn_subagent gains agent: Option<String>: definition body becomes the system prompt, tools: frontmatter filters the subagent toolset (Claude Read/Glob/Grep/Bash/Edit/Write/Sleep naming), model overrides the child model, permissionMode overrides inherited mode (Auto stays sticky) - subagent_toolset_for(keep) filtered toolset builder; unknown names fall back to the default five-tool set
…mptSubmit/Pre/PostToolUse/SubagentStart) - plugin/hooks.rs: parses Claude hooks JSON (matcher/command/commandWindows/ timeout/statusMessage/async); run_command_hook executes via sh -c with CLAUDE_PLUGIN_ROOT/CLAUDE_PROJECT_DIR env, JSON payload on stdin, stdout JSON parsed in both decision and hookSpecificOutput formats; failures/ timeouts/invalid JSON warn and continue (fail-open) - Hook enum gains UserPromptSubmit (invoked in agent_loop on the user message text; additionalContext appended); SubagentStart is a standalone trait stored on ToolContext (no LoopState in the spawn tool path) and invoked by spawn_subagent to inject context into the child system prompt - apply_plugin_hooks registers installed plugins' SessionStart/ UserPromptSubmit/PreToolUse/PostToolUse command hooks on the Agent builder (interactive + headless); with_post_tool_hook added (with_post_tool is rtk_filter-gated) - plugin_subagent_start_hooks builds SubagentStart closures for ToolContext - TUI plugin list table shows skills/commands/agents/hooks/mcp columns (en/zh)
- Ch 2: commands/*.md load as plugin:<name>, frontmatter fields, precedence - Ch 8: installed-plugin MCP scan + .mcp.json, http/url limitation - Ch 9: Hook table gains UserPromptSubmit; SubagentStart standalone; plugin command-hook protocol section (events, matcher, env, dual output formats, fail-open semantics) - Ch 12: declarative agent definitions (.tact/agents + plugin agents), spawn_subagent agent field, tools/model/permissionMode mapping - Ch 23: /plugin list feature table; Ch 26 entry (bilingual)
…ext stdout fallback - hook input payload merges event fields at the top level (Claude protocol puts tool_name/tool_input/prompt at the root), not nested under _event - parse_output accepts plain-text stdout as additionalContext for UserPromptSubmit/SessionStart (ponytail's native-Claude path writes raw text; JSON-only parsing silently dropped its context)
… items-after-test-module)
… model aliases, tool-set safety, Block semantics Review (subagent acceptance audit, ~75%) found real compatibility gaps; fixed: - hooks: default discovery path hooks/hooks.json when plugin.json lacks the hooks field (6 official marketplace plugins rely on it); unparseable manifests now warn; InstalledHooks no longer needs plugin_id - declarative agents: model frontmatter aliases — inherit = no override, sonnet/opus/haiku warn+ignore (never passed to provider), concrete ids pass through (resolve_agent_model) - subagent toolset: all-declared-tools-unknown now yields an empty router and spawn_subagent errors out instead of silently widening to the full default set (permission safety) - UserPromptSubmit Block now drops the turn (Claude semantics) instead of appending a marker; apply_user_prompt_hooks returns HookControl - SubagentStart plugin-hook Block propagates to spawn_subagent (bail); the closure writes back to the caller's context instead of a clone; payload adds agent_type (ponytail PONYTAIL_SUBAGENT_MATCHER scoping reads it) - timeout: 0 disables the timeout (was coerced to the 60s default) - new-format suppressOutput parsed at the top level - SessionStart plain-text/additionalContext output warns explicitly - statusMessage surfaced via tracing::debug - tests: hooks default-path load, SubagentStart block propagation, timeout-0, new-format suppressOutput, mixed known/unknown toolset, resolve_agent_model, installed_plugin_roots; docs spec/chapters/Ch26 synced
…timeout resolution unit test, user_text_target tests - subagent_toolset_for: an empty tools: list (Some(&[])) keeps the default five-tool set (Claude semantics: absent/empty tools: does not restrict); only non-empty fully-unknown lists yield an empty router for spawn bail - resolve_timeout extracted as a pure function; unit-tested (None→60, Some(5)→5, Some(0)→disabled) so the timeout-0 regression test is real - user_text_target unit tests: mutates Text content and the first text block while preserving non-text blocks; returns None without a text block
…cel slash + tool-card [Cancel] button
- SubagentManager gains an in-memory cancel-handle registry
(register_cancel_handle / request_cancel / unregister_cancel_handle);
spawn_subagent registers the child's runtime.cancel_flag after Agent::new
and unregisters on both sync and async finish
- new cancel_subagent tool (flips the cooperative flag + marks the run
Cancelled); registered in the main toolset
- async finish path detects the flag: cancelled runs are recorded as
Cancelled (not Completed) and the re-injected summary is prefixed
(cancelled by user) with success=false
- protocol: UserCommand::CancelSubagent; driver flips the flag directly
(works while the parent task is mid-turn)
- TUI: /subagent_cancel <child-id> slash command (usage flash without
args); live async-subagent tool cards render a [Cancel] button — child id
parsed from the async_launched { id } result, button rects returned by the
pure renderer and routed through mouse state
- tests: manager handle lifecycle, main-toolset registration, slash command
with/without id, mouse button click, parse_async_launched, keep-live child
id capture; docs Ch 12 gaps + Ch 26 bilingual
Replace the single Option<SubagentPopup> slot with a HashMap keyed by tool id plus an active-subagent pointer, so switching between concurrent subagents preserves each card's scroll / selection / cached layout. - App.subagent_popup -> subagent_popups + active_subagent_popup - open_subagent_popup insert-or-reuses each card's entry - new accessors: subagent_popup / subagent_popup_mut / has_subagent_popup / close_subagent_popup - updated all overlay/mouse/layout/render/config/construct call sites
Let the parent block on a running background subagent until it reaches a
terminal status (Completed/Failed/Cancelled) or a timeout elapses, returning
the summary — so the parent can spawn N subagents then wait on each instead
of burning turns polling check_subagent.
- SubagentManager::wait(child_id, timeout_ms) polls subagent_runs (250ms)
- SubagentManager::get(child_id) -> Option<SubagentRun>
- new Read/Independent tool wait_subagent { child_id, timeout_ms? } (default 60s)
- registered in the main toolset
Isolated subagent lanes (subagent-<child_id>) previously leaked until a
manual git worktree remove. Add a first-class cleanup tool.
- WorktreeManager::remove(name): git worktree remove (no --force, dirty tree
fails), delete tracking row, append audit event, keep wt/<name> branch
- WorktreeStore::remove_worktree(name) -> bool
- new Write/Barrier tool worktree_remove { name }; refuses a subagent-<id>
lane whose run is still Running
- registered in the main toolset
- book/12 (en+zh): wait_subagent + worktree_remove fields and lifecycle - book/15 (en+zh): five -> six worktree tools, remove semantics, audit log, gap table updates - book/26 (en+zh): 2026-09-02 async-subagent follow-ups entry
The driver dropped SubagentFinishedNotification whenever a turn was active. A result landing in the gap between the final queue drain and turn exit was never re-injected: the parent stayed silent until the next manual turn. Select on the in-flight JoinHandle alongside user_cmd_rx, retain a pending_subagent_wakeup flag, and submit the wake-up turn as soon as the active turn completes. SubmitTask clears the flag since the new turn drains the queue itself.
The async completion task emitted SubagentFinished with the raw agent_loop result, so a cancelled child that exited cleanly (Ok after the flag was set) was reported success=true while the queued SubagentResult was already success=false. Use one terminal_success(success, cancelled) helper for both.
resume blindly reused any id: resuming a still-Running child would race the session (two loops on one session), and resuming an unknown id silently minted a fresh child instead of a follow-up. Reject both before any spawn work via SubagentManager::get.
Sync children registered a cancel handle but never wrote a subagent_runs row, so check_subagent / cancel_subagent were blind to them and a failed sync spawn left a stale Running row. Move manager.start above the sync/async split and record Completed/Failed/Cancelled on the sync exit path (also unregister and release the lock before propagating an error). run_in_background also needs an interactive UI channel: in headless there is no driver to submit a wake-up turn and the run ends by cancelling the child. Degrade to synchronous (with a warning) when ui_tx is absent so the summary still reaches the parent.
Document the five async-subagent reliability fixes in Ch 12 (wake-up retention, cancellation success, resume validation, sync lifecycle, headless degradation) and append a bilingual Ch 26 entry.
Subagents previously could not spawn further subagents (depth 0 by design), so a worker could never decompose work further. Give ToolContext a subagent_depth (0 = main agent), register spawn/check/wait/cancel_subagent in subagent_toolset() (9 tools; Claude Task maps to spawn_subagent), and refuse to spawn beyond MAX_SUBAGENT_DEPTH = 3. The child's tool context carries depth + 1 so the same guard applies to grandchildren.
resume had no expiry policy (TBD in the 2026-08-26 design), so a weeks-old finished session could be resumed with stale context. Reject resume targets whose finished_at is older than RESUME_EXPIRY_HOURS = 24 (Claude's 24h expiry) and cover the window check with a unit test.
worktree_remove always left the backing wt/<name> branch behind, requiring a manual merge or git branch -D. After git worktree remove, attempt git branch -d — only a fully-merged branch is deleted; unmerged commits are never destroyed and the outcome is reported and audit-logged.
worktree_run executed arbitrary shell without the validate_shell_command gate (unique among the shell tools) and wrote no audit log. Apply the same validation as bash and append 'worktree.run <name> <command>' to the worktree_events audit log so every lane command is inspectable.
Manual 'git worktree remove' / 'git worktree prune' left stale records in the worktrees table forever (index drift). WorktreeManager::new now repairs orphans: any tracked lane whose on-disk path is missing is dropped and logged as 'worktree.stale-removed <name> (path missing)'.
Ch 7: subagent toolset is 9 tools (nested spawn + check/wait/cancel), gap row updated. Ch 12: nested spawn depth limit, 24h resume expiry, nine-tool set. Ch 15: branch cleanup on remove, run validation + audit, startup reconciliation; gaps trimmed. Ch 26: bilingual entry for all five leftovers.
Mirror the revert of 6c32665: Ch 7/12 back to the five-tool set and no-nested-spawn wording, ARCHITECTURE restored, the leftover Ch 26 entry rewritten without nested spawn, and a bilingual removal entry appended.
Requests to OpenCode Go (https://opencode.ai/zen/go/v1) carried no x-opencode-session header and only reqwest's generic User-Agent, so the endpoint could not correlate the session and reported 'Unknown client'; starting 09/06 missing the header may error. Add an opencode helper module that detects opencode.ai endpoints and emits a per-process, per-base_url session token plus a tact/<version> User-Agent. Attach the headers on every path to the endpoint: the Responses SDK config (create_byot / create_stream_byot), the direct /responses/compact POST, the Chat Completions config (defensive), and the /v1/models picker fetch. TACT_OPENCODE_SESSION pins the session value when set.
Ch 21 custom-provider section notes that OpenCode Go endpoints (opencode.ai and subdomains) automatically receive a stable x-opencode-session header and a tact/<version> User-Agent (override with TACT_OPENCODE_SESSION); Ch 26 gains a bilingual bugfix entry.
The first OpenCode fix used a per-process, per-base_url token as x-opencode-session. OpenCode treats the header as the session key that distinguishes per-conversation caches, so different Tact sessions sharing a token would share/pollute each other's cache and resume would not continue the same cache. Reuse the LlmProvider::set_user_id hook that Agent::with_session already calls for DeepSeek KV-cache isolation: the OpenAI Responses adapter now stores the session id and emits x-opencode-session = <session id> on the SDK config and the /responses/compact POST. Non-conversation requests (the /v1/models picker fetch) still fall back to the per-base_url token; TACT_OPENCODE_SESSION pins that fallback only.
Ch 21 updated to say the header value is the session id (same session and resumed sessions reuse one value, different sessions differ) with the per-base_url fallback only for non-conversation fetches; Ch 26 gains a bilingual refinement entry above the original OpenCode entry.
Stop syncing book / design docs after every intermediate edit — that burns tokens re-reading chapters mid-work. Agents now batch doc updates: code first, then compare the final diff against the trigger table once, before pushing, and sync all touched docs in a single pass.
…ints
Every /responses request replayed historical assistant reasoning items
(persisted signatures with opaque encrypted payloads) into input. Official
OpenAI needs that for turn continuation; compatible endpoints (OpenCode Go,
custom OpenAI-compatible proxies) regenerate reasoning each turn, so
replaying every previous chain of thought is pure input-token waste
(~17% of request bytes, up to ~47% of tokens on DeepSeek-style endpoints).
Add a per-adapter replay_prior_reasoning policy:
- OpenAiResponsesAdapter derives the default from the base URL
(is_official_openai_base_url: api.openai.com / Azure OpenAI -> replay,
everything else -> drop); with_replay_prior_reasoning overrides it.
- create_response becomes create_response_with_policy carrying a
ResponsesRequestPolicy { native_web_search, replay_prior_reasoning }.
- The reasoning signature is still decoded on every path so fc_* function
call item ids stay attached to their function_call items; only the
standalone reasoning payload is omitted.
- With replay disabled, stale reasoning items are filtered from the
persisted state baseline before the body is built and the returned
input_items exclude reasoning, so older persisted states self-heal.
Ch 22 §6.2.3 documents the new replay policy (base-URL default + override), and §6.2 / §6.2.2 now scope the earlier "next request replays reasoning" statements to official OpenAI. Ch 26 issue log gains the 2026-09-05 optimization entry in both languages.
OpenCode's hosted endpoints no longer require an x-opencode-session header (nor a custom tact/<version> User-Agent), so the mechanism that detected opencode.ai base URLs and attached the header to every request was dead weight. It also forced plumbing the Tact session id (Agent::with_session -> LlmProvider::set_user_id -> OpenAiResponsesAdapter::set_session_id) purely to feed one header value. Delete crates/tact_llm/src/opencode.rs and stop attaching the header on the Responses SDK config, the direct /responses/compact POST, the Chat Completions config, and the /v1/models picker fetch. Drop the session_id field / set_session_id from the Responses adapter and the OpenAiResponses arm of set_user_id. Remove the TACT_OPENCODE_SESSION env override. opencode.ai endpoints are now treated like any other OpenAI-compatible base URL; DeepSeek user_id KV-cache isolation is unaffected. Syncs Ch 21 and adds a Ch 26 removal entry.
Like Claude Code's user-level ~/.claude state, memories now live in $HOME/.tact/memory so they survive across projects. The legacy <workdir>/.tact/memory path stays as a fallback only when $HOME is unset; save_memory confirmations render ~/... paths. Docs (store, skill, memory, prompt chapters, ARCHITECTURE) synced en/zh.
…tus serde Replace duplicated status_to_str/str_to_status helpers in the sqlite stores with From<Status> for &'static str and TryFrom<&str> on the enums, and refresh the spawn_subagent tool description.
spawn_subagent no longer accepts `agent: <name>`; every child runs on the generic static system prompt with the fixed subagent_toolset() five tools. - delete agent_def.rs (registry + frontmatter parsing); drop the ToolContext.agent_registry field and TactPath::agents_dir - subagent.rs: remove SubagentInput.agent, resolve_agent_model, and the spawn-time prompt/permission/model/toolset overrides; SubagentStart hooks keep running with the child_id as the hook name - registry.rs: remove subagent_toolset_for / allowed_tool_names and the filtered toolset builder - plugins no longer count or advertise agents/*.md: drop InstalledPlugin/PluginFeatures.agent_count from install bookkeeping, tact plugin list, and the /plugin table; an agents/-only plugin is now rejected at install (test install_rejects_agent_only_plugin) - docs: Ch 7/12/21 en+zh drop declarative-agent wording; Ch 26 en+zh get a removal entry (2026-09-06) [agent.subagent] config and /model-subagent are unaffected.
Restore reusable worker roles after agent-def removal (8c74f4e) with a deliberately minimal, isolated mechanism: - SubagentInput gains an opt-in `skill: <name>`; the handler reads ~/.tact/subagent/<name>.md (key = file stem; frontmatter description is only for listings) and appends its body as a <skill> block to the child's static system prompt before SubagentStart hooks run. - Names must be plain file stems (no separators / `.`/`..` / NUL) so a skill value cannot read outside the card directory; unknown names fail the spawn and list available cards. Catalog listing follows symlinks to stay consistent with the resolver. - ToolRouter gains set_tool_description so interactive/headless annotate the spawn_subagent tool description with the card catalog at startup (single-line, 60-char-capped descriptions, 30-card cap) for model discovery; empty/absent dir leaves the description unchanged. - Cards never enter SkillRegistry, plugin side, or ToolContext; subagent toolset stays at five tools. - Bilingual Ch 12 + Ch 26 synced; spec/plan added.
Add a persistent status overview for subagents in the TUI, mirroring the Tasks sticky strip, on the current component architecture (no rollback of 98a133f's tool-card detail direction). - protocol: SubagentStatusSnapshot, SubagentRunSnapshot, and AgentUpdate::SubagentsChanged { runs } full-snapshot event - tact: SubagentManager tracks children started by this process (known set); emit_subagents_changed after spawn start / sync+async finish / cancel_subagent tool / driver CancelSubagent - agent_tui_kit: SubagentPanelComponent/State + two-domain sticky host (render/sticky_host.rs) with [Tasks] [Subagent] tab segments returning per-frame hit areas - tui: sticky host integration (layout, mouse/normal handlers, registry), tab click switches/expands domain, wheel/jk scroll the active domain - Live subagent detail stays on the tool card / SubagentPopup; the sticky shows status-level runs only (Running -> Completed -> Failed -> Cancelled), capped with all Running preserved Docs: Ch 12 / Ch 23 / Ch 26 (EN+ZH); design spec and plan under docs/superpowers/{specs,plans}/2026-09-07-subagent-sticky-tab*.md
Remove 47 outdated design specs and execution plans whose features are shipped and documented in book chapters / code. Keep the two files still linked from stable docs and code (task-progress-panel-design, referenced by Ch 19; tui-component-library plan, referenced by agent_tui_kit) plus the latest three design+plan sets (claude-plugin-compat, skill-cards, subagent-sticky-tab).
Branch's pre-push hook (scripts/check-rust.sh: fmt + clippy -D warnings + tests) was failing on pre-existing issues. Clean up so the hook passes: - cargo fmt across workspace (stale formatting from earlier commits) - tui: drop unused task_panel/subagent_panel re-export modules and dead sticky_tab_visible/sticky_tab_scroll helpers - tui: iterate slices instead of index-only loops (needless_range_loop) - Remove unused deps: dashmap + tracing-subscriber (workspace/tact/tui), tokio (protocol, agent_tui_kit); sync Cargo.lock - Drop misleading workspace rust-version=1.85 (unused; code needs 1.91+)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the plugin-compat workstream to
main: Claude Code plugin compatibility (skills/commands/agents/hooks/MCP), a hardened subagent system, user-global memory, and a sticky-tab task/subagent overview.Highlights
Plugin compatibility (Claude Code)
commands/*.mdas slash commands.mcp.json+mcpServers).tact/agents+ pluginagents/*.md)Subagent system
cancel_subagenttool +/subagent_cancelslash + tool-card [Cancel] buttonwait_subagenttool; multi-popup for concurrent subagent transcriptsworktree_run/worktree_remove), reconcile index with git on startupspawn_subagent skill:; Subagent sticky-tab overview under the LogLLM
x-opencode-session+ tact User-Agent to OpenCode Go; bind header to the Tact session id/responsesendpointsMemory / store
~/.tact/memory(user-global)From/TryFromserde impls for background & subagent status;memory_manager()factory (dropget_prefix)Housekeeping
-D warningscleanup; remove unused depsVersion: bump to v1.1.26
Test plan
scripts/check-rust.shpasses locally and on push (fmt + clippy-D warnings+ unit/integration tests)crates/tactNotes
51 commits ahead of
main; fast-forwardable.