Skip to content

epic: Enable agents to discover Projects and control threads #958

Description

@chuks-qua

Problem Statement

Mcode agents cannot currently discover the user’s Projects, create threads in another Project, read existing threads, send cross-thread messages, stop work, or wait for results through an Mcode-owned tool surface. The application already supports much of this behavior through internal services and WebSocket methods, but provider agents cannot call those capabilities.

This prevents a coordinator thread from handling workflows such as an epic with several child issues. The user cannot ask one thread to find the relevant Projects, create one isolated worktree thread for each issue, assign the work, monitor progress, and follow up across those threads.

Mcode also needs two distinct trust models. An agent running inside Mcode acts for the current user and should work across the user’s registered Projects without per-thread grants. A separately paired external MCP integration must be limited to selected Projects, explicit operations, owned threads where applicable, and bounded capacity.

Solution

Add a provider-neutral thread-control domain and a shared server service that lets authorized callers:

  • Search registered Projects and resolve each Project to its opaque workspaceId.
  • List known worktrees for a Project.
  • Create and start one to twenty normal Mcode threads across Projects.
  • Choose direct, new-worktree, or existing-worktree placement.
  • Search and read existing threads.
  • Send messages to other threads.
  • Stop active work.
  • Wait for exact threads to require attention or reach a terminal state.

Expose this service first through an internal provider-session MCP. Add a separate paired external MCP that reuses the same service through a stricter authority context. Both MCP adapters authenticate the caller and construct its authority. ThreadControlService enforces that authority for every operation.

Persist creator lineage and cross-thread message provenance. A created thread remains a normal visible Mcode thread in its destination Project. The source timeline may also show its relationship and status.

User Stories

  1. As a user, I want to ask an Mcode agent to find one of my Projects, so that I do not need to provide an internal identifier.
  2. As an agent, I want to search registered Project names and repository identities, so that I can select the intended destination.
  3. As an agent, I want every Project result to include workspaceId, so that I can target the Project exactly.
  4. As a user, I want ambiguous Project searches to return every match, so that the agent does not guess.
  5. As an agent, I want an empty Project search to return a bounded recent list, so that I can discover likely destinations without searching the filesystem.
  6. As a user, I want Project search limited to Projects already registered in Mcode, so that agents cannot browse arbitrary directories.
  7. As an agent, I want to list known worktrees for a Project, so that I can attach work to an existing checkout.
  8. As a user, I want agents to use opaque worktree IDs instead of filesystem paths, so that path resolution stays inside the server.
  9. As a user, I want a thread to create another normal Mcode thread, so that delegated work remains visible and controllable.
  10. As a user, I want a thread to create work in another Project, so that one coordinator can manage work across my registered Projects.
  11. As a user, I want one request to create up to twenty threads, so that an epic can fan out to several child issues.
  12. As a user, I want each batch item to target a different Project when needed, so that one workflow can span repositories.
  13. As a user, I want successful batch items to remain active when another item fails, so that one bad assignment does not discard valid work.
  14. As an agent, I want one structured result for every batch item, so that I can understand what was created, rejected, left pending, or failed.
  15. As an agent, I want creation results to include workspaceId, threadId, resolved provider, resolved model, permission mode, interaction mode, placement, and initial-turn state, so that I can act on the result without another discovery call.
  16. As a user, I want agents to create direct threads, new-worktree threads, and existing-worktree threads, so that they can use Mcode’s normal placement choices.
  17. As a user, I want new worktrees created from an explicit base ref and optional branch name, so that repository placement is predictable.
  18. As a user, I want an existing worktree accepted only when it belongs to the selected Project, so that a mismatched checkout cannot be targeted.
  19. As a user, I want omitted provider and model values to resolve from my configured defaults, so that routine creation stays concise.
  20. As a user, I want an explicitly selected provider or model honored exactly or rejected, so that Mcode never substitutes a different runtime silently.
  21. As a user, I want omitted internal permission mode to resolve from my configured default, so that agent-created threads follow my normal policy.
  22. As an external integration owner, I want omitted permission mode to resolve to Supervised unless the integration has Full execution authority, so that pairing does not inherit excessive access.
  23. As a user, I want Build to be the default interaction mode, so that normal delegated work starts implementation.
  24. As a user, I want an agent to choose Plan explicitly for research, architecture, or unresolved work, so that a child can investigate before implementing.
  25. As an agent, I want follow-up messages to select Build or Plan, so that I can move a thread from planning to implementation.
  26. As a user, I want agents to search and read existing threads, not only threads they created, so that they can inspect related work when asked.
  27. As a user, I want commands such as “read the last ten open threads” to work across registered Projects, so that I can request broad context without tagging each thread.
  28. As a user, I want to tag an exact thread for an agent to read, so that the agent can inspect the intended conversation directly.
  29. As a user, I want the active source thread excluded from its own search and control tools, so that it cannot recursively target itself.
  30. As an agent, I want bounded thread reads with explicit observed status, so that I can inspect enough history without loading an unlimited transcript.
  31. As a user, I want agents to send messages to other threads, so that a coordinator can clarify or extend delegated work.
  32. As a user, I want agents to stop other threads when their authority permits it, so that a coordinator can cancel obsolete or harmful work.
  33. As a user, I want Supervised cross-thread mutations to request my approval, so that I retain control under Supervised mode.
  34. As an agent, I want a pending mutation to return an opaque approvalId, so that I can correlate the request with later thread observations.
  35. As a user, I want only a human to resolve approval requests in the first release, so that agents cannot approve their own or each other’s actions.
  36. As a user, I want a pending new-worktree thread visible before approval, so that requested work does not disappear while waiting.
  37. As a user, I want repository mutation and turn dispatch delayed until approval, so that a pending request has no side effects.
  38. As an agent, I want to wait for exact threads, so that I can coordinate several assignments without polling broad state.
  39. As an agent, I want the default wait boundary to be attention or terminal, so that I wake for approval, user input, completion, failure, or stoppage.
  40. As an agent, I want to choose a terminal-only wait, so that intermediate attention states do not complete the wait.
  41. As an agent, I want timeout results to preserve each thread’s authoritative state, so that a deadline does not obscure ongoing work.
  42. As a user, I want cross-thread messages to record their source thread, source turn, and source provider, so that I can see where an instruction came from.
  43. As a user, I want composer messages distinguished from thread-origin messages, so that the interface does not misrepresent an agent message as my direct input.
  44. As a user, I want the recorded source provider icon shown for a cross-thread message, so that historical provenance remains clear after provider changes.
  45. As a user, I want old messages without provenance to remain readable, so that the migration does not discard existing conversations.
  46. As an internal agent, I want access to all registered Projects and all other threads, so that normal local workflows do not require per-thread grants.
  47. As an external integration owner, I want to select the Projects an integration may access, so that pairing does not expose every Project.
  48. As an external integration owner, I want separate read, create, send, stop, worktree, and Full execution scopes, so that I can grant only required operations.
  49. As an external integration owner, I want owned-thread scopes distinct from Project-wide scopes, so that an integration can manage its own work without reading or changing user-created threads.
  50. As a user, I want unauthorized and unknown external targets to return the same normalized response, so that the API does not reveal hidden resources.
  51. As a user, I want external rate and active-thread limits enforced atomically, so that concurrent batches cannot exceed configured capacity.
  52. As an agent, I want clear structured errors with a code, message, and retryability, so that I can decide whether another attempt is useful.
  53. As a user, I want retry decisions left to the agent, so that the public contract does not require model-managed request IDs.
  54. As a user, I want accidental transport replay suppressed internally, so that one delivered call does not execute twice.
  55. As a user, I want intentional repeated calls treated as distinct actions, so that replay protection does not merge separate requests.
  56. As a user, I want created threads and pending threads to survive source-turn completion and application restart according to a defined lifecycle, so that durable work does not depend on one provider call.
  57. As a user, I want every cross-thread operation audited with source, target, action, and outcome, so that agent activity can be investigated without logging secrets or full prompts.
  58. As a user, I want agent-created threads to appear in the destination Project’s normal thread list, so that delegated work remains part of Mcode rather than hidden provider state.
  59. As a user, I want the source timeline to link to created threads and show their state, so that I can move between coordination and execution.
  60. As a developer, I want one provider-neutral service to enforce internal and external policy, so that adapters cannot drift or bypass authorization.

Canonical implementation contract

The Agent Thread Control Contract is the source of truth for model-facing inputs, result unions, server-owned authority, limits, defaults, approvals, provenance, concurrency, lifecycle, and delivery ownership. PR #967 introduces it. Later tickets must implement that contract rather than reconstructing shapes from this issue or provider-specific APIs.

Implementation Decisions

  • Use Project in user-facing language. Keep Workspace and workspaceId as the existing code and contract terms. workspaceId is the opaque Project identifier.
  • Build the internal provider-session MCP first. Design and implement the shared service so that a separately paired external MCP can reuse it.
  • Add a dedicated thread-orchestration package for provider-neutral authority types, state transitions, capability policy, replay protection, and lineage. Keep wire schemas and public event types in the contracts package.
  • Add ThreadControlService in the server. It coordinates existing thread, agent, settings, worktree, persistence, approval, and event services.
  • Keep MCP adapters thin. Each adapter authenticates its caller, constructs a server-owned authority object, translates the request, and calls ThreadControlService.
  • Enforce authority inside ThreadControlService, not only at the MCP boundary, so that other server callers cannot bypass policy.
  • Represent internal authority with the authenticated user, source thread, source turn, source tool call, source provider, and current permission mode. The model cannot submit or override these fields.
  • Internal callers can search all registered Projects and control any other thread. They have no per-Project or per-thread allowlists. Search and all target operations exclude the source thread.
  • Represent external authority with the integration identity, selected Workspace IDs, operation scopes, rate limit, and active-thread limit. The model cannot submit or override these fields.
  • Support external scopes for Project search, worktree reads, thread creation, owned-thread reads, Project-thread reads, owned-thread sends, Project-thread sends, owned-thread stops, Project-thread stops, worktree creation, and Full execution.
  • Require both an allowed workspaceId and the relevant operation scope for every external action. Apply ownership restrictions when the scope covers only integration-created threads.
  • Normalize unknown, unauthorized, unselected, unreadable, and source-thread targets to
    ot_foundwhere existence must remain hidden. OmitworkspaceId` unless the caller is already authorized to know the Project.
  • Expose workspace_search. It accepts an optional bounded query and limit. It searches registered Project names and repository identities. An empty internal query returns recently used Projects. External results remain limited to selected Workspaces and the Project-read scope.
  • Expose worktree_list. It accepts workspaceId and returns opaque worktree IDs with user-readable labels and available branch identity. It never returns a raw filesystem path.
  • Expose thread_create_batch. It accepts one to twenty items. Each item requires workspaceId, title, prompt, and placement. Provider, model, permission mode, and interaction mode remain optional.
  • Model placement as a discriminated choice between direct, new worktree, and existing worktree. New-worktree placement requires a base ref and may include a branch name. Existing-worktree placement requires an opaque worktree ID.
  • Resolve omitted provider and model from the user’s configured defaults. Honor explicit values exactly or reject the item.
  • Resolve omitted internal permission mode from the user’s configured default. Resolve omitted external permission mode to Supervised unless execution:full permits the user default. Reject explicit external Full without that scope.
  • Default interaction mode to Build. Permit an explicit Plan choice on creation and follow-up messages.
  • Return one discriminated result per creation item. A created result includes Project, thread, turn, resolved execution settings, worktree identity when applicable, and starting, running, or failed run state.
  • Return pending_approval when a visible thread exists but a required mutation has not been approved. Include workspaceId, threadId, approvalId, resolved execution settings, and a waiting-for-approval state. Omit turnId until dispatch begins.
  • Return failed when a visible persisted thread exists but provisioning or dispatch failed. Return rejected when validation or authorization fails before persistence.
  • Preserve partial batch success. For external callers, reserve active capacity atomically in request order before persistence. Reject unreserved items with a retryable limit error.
  • Keep semantic retries outside the protocol. Do not expose a model-managed request or idempotency key. Suppress duplicate transport delivery internally without merging intentional calls.
  • Expose thread_search. It accepts an optional bounded workspaceIds filter, optional bounded text, bounded status filters, and a bounded limit. Omission searches every Workspace the caller may read, and every result includes workspaceId. It returns thread IDs, titles, resolved provider and model identities, timestamps, and discriminated observed state.
  • Expose thread_get. It accepts a thread ID and bounded message limit. It returns the thread summary and bounded messages or a normalized rejection.
  • Persist inbound message origin as composer, thread, or legacy. New composer writes use composer origin. New cross-thread writes use source thread, source turn, and source provider derived from the authenticated session. Legacy exists only for rows created before provenance support.
  • Persist provider and model identities on assistant messages. Treat all recorded provenance as historical and never infer it from a thread’s current provider.
  • Expose thread_send. It accepts target thread, message, optional interaction mode, and optional permission mode. It returns accepted, pending approval, or rejected.
  • Expose thread_stop. It accepts a target thread and returns the same action-result variants as send.
  • Internal Full mutations execute directly. Internal Supervised mutations return pending_approval. Only the user may resolve approvals in the first release.
  • Persist a pending new-worktree thread before requesting approval. Perform no repository mutation or turn dispatch until the human approves. Resume the same operation after approval.
  • Expose thread_wait. It accepts a bounded set of thread IDs, an optional attention-or-terminal or terminal boundary, and a bounded timeout.
  • Default wait behavior to attention or terminal. Require every target to reach the boundary before successful completion unless the timeout expires. Preserve authoritative status for every result when timed out.
  • Reject the entire wait without partial results if any target is invalid or outside the caller’s read authority.
  • Represent observed states as starting, running, idle, completed, failed, stopped, waiting for approval, or waiting for user input. A waiting-for-approval state carries the exact approval ID; other states omit it.
  • Add durable creator lineage to a created thread: parent thread, creator turn, creator tool call, and a creation kind. Keep this relationship separate from conversation forks.
  • Persist every accepted thread before starting its initial task. Keep a failed thread visible when later provisioning or dispatch fails.
  • Add database migrations for creator lineage, creation ownership for external integrations, message origin, approval correlation, orchestration state, and any replay records needed by the transport boundary.
  • Backfill existing inbound user messages with a readable legacy origin. Do not permit new writes to create legacy origin.
  • Bound batch size, search results, transcript reads, title and prompt length, nesting depth, active threads, message size, wait duration, and provider tool exposure.
  • Follow ADR 0021 for the internal and external authority split, human-only approvals, busy-thread behavior, send ordering, and source lifecycle.
  • Prototype the source timeline item and cross-thread message treatment after contract and lifecycle behavior stabilizes, but before production UI work.

Testing Decisions

  • Test observable behavior at the ThreadControlService boundary as the primary seam. This seam covers both MCP adapters while avoiding duplicate tests for the same policy.
  • Add contract-schema tests for every tool input, output, discriminated outcome, bound, and invalid combination.
  • Add policy tests for internal authority, external Workspace selection, every external scope, ownership rules, source-thread exclusion, and normalized non-enumerating errors.
  • Add service integration tests for Project search, recent Project discovery, ambiguous results, worktree listing, and opaque-ID resolution.
  • Add creation tests for direct, new-worktree, and existing-worktree placement across Projects.
  • Test default provider, model, permission, and interaction resolution. Test exact overrides and rejection without substitution.
  • Test one-to-twenty-item batches, partial success, invalid items, and atomic external capacity under concurrent requests.
  • Test the distinction between rejected, pending-approval, failed, and created outcomes.
  • Test that a pending worktree thread is persisted before approval and that no repository mutation or turn dispatch occurs before human approval.
  • Test approval, rejection, and resumed execution against the same pending operation and approval ID.
  • Test thread search and reads for unrelated existing threads, external owned-thread access, Project-wide external access, and source-thread exclusion.
  • Test cross-thread send and stop under internal Full, internal Supervised, external owned scopes, and external Project scopes.
  • Test message provenance for composer messages, cross-thread messages, legacy rows, and historical provider identity after a provider switch.
  • Test bounded transcript reads and ensure raw paths never cross the contract boundary.
  • Test wait behavior for attention states, terminal states, timeouts, malformed input, mixed states, and unauthorized targets. Confirm that timeout does not cancel, retry, or change work.
  • Test transport replay suppression separately from semantic calls. Confirm that one duplicate delivery executes once and two intentional calls remain distinct.
  • Add migration tests that preserve existing conversations, expose legacy origin on old user messages, and keep new provenance mandatory.
  • Add server integration tests proving that both internal and external MCP adapters construct authority correctly and cannot override caller-derived fields.
  • Add live runtime tests for the complete coordinator workflow: search a Project, create a thread in another Project, read it, send a follow-up, wait for attention or completion, and stop it.
  • Add live UI checks for destination Project visibility, source-thread navigation, pending approval, source provider icon, and historical provenance.
  • Use the existing thread, agent, worktree, approval, settings, and WebSocket integration tests as prior art. Extend their public seams rather than testing private helper functions.
  • Run the focused package and server gates during each slice. Run the repository verification gate for cross-package contract changes.

Out of Scope

  • Agent approval of approval requests.
  • Per-thread readable or unreadable settings for internal agents.
  • Per-Project allowlists for internal agents.
  • Arbitrary filesystem Project discovery.
  • Raw filesystem paths in model-facing contracts.
  • A separate orchestration process or standalone application.
  • Replacing Mcode’s thread, agent, worktree, settings, or approval services.
  • Treating provider-owned subagent sessions as durable Mcode threads.
  • Relaxing existing cross-Project conversation-fork restrictions.
  • Public model-managed request IDs or semantic retry counters.
  • Timeline feed items in thread-creation results.
  • Unbounded batches, reads, waits, or retries.
  • Agent-created approval policies.
  • Final production UI design before the prototype checkpoint.
  • Conversation forks, deep recursive nesting, richer audit views, and full restart recovery beyond the lifecycle required by the first implementation.

Further Notes

  • The primary use case is epic coordination: one thread finds eligible child issues, creates one isolated worktree thread per issue, names each thread with its issue number, assigns the work, and monitors the results.
  • Codex provides useful patterns for strict tool registries, bounded spawning, thread and turn lifecycle operations, and streamed state. Synara provides useful separation between internal provider-session tools and paired external MCP authority. Mcode must keep threads as normal cross-provider Mcode records because provider-owned child sessions cannot own Mcode Projects, worktrees, permissions, or persistence.
  • The static contract review covers fourteen scenarios and passes all fourteen.
  • ADR 0021 records the settled authority and lifecycle rules: busy destinations return thread_busy; simultaneous sends use an atomic first-accepted gate; created threads continue independently; source waits end when the source stops or disconnects; deleting the source does not delete created threads; and only the human user resolves approvals.
  • Ticket sequencing should begin with the shared contracts and durable foundation, then internal discovery, creation, interaction and provenance, waiting, external authority, prototype, production UI, and end-to-end verification.

Sequencing

#959
├── #960
└── #961
     ↓
#960 + #961
     ↓
    #962
   ┌──┴──┐
 #963   #964  can parallel
   └──┬──┘
      ↓
    #965

Child issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicEpic: multi-feature business capabilityready-for-agentTriaged; ready for an autonomous coding agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions