diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 0ef02d8b..f09cdb4e 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -191,6 +191,12 @@ public sealed record InitializeResult
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? DefaultDirectory { get; init; }
+ /// Host-owned repository preparation for session creation and repository
+ /// context in configuration queries. Absence means unsupported; an empty
+ /// object supports one repository at its default revision.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public RepositoryPreparationCapabilities? RepositoryPreparation { get; init; }
+
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -271,6 +277,22 @@ public sealed record ClientCapabilities
public Dictionary? McpApps { get; init; }
}
+/// Repository preparation supported by this host, independent of the selected
+/// agent. Resulting working directories must still fit that agent's existing
+/// directory capabilities.
+public sealed record RepositoryPreparationCapabilities
+{
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Revision { get; init; }
+
+ /// When true, clients may supply more than one repository. When absent or
+ /// false, the host MUST reject lists with more than one entry with
+ /// `InvalidParams` before preparation.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? MultipleRepositories { get; init; }
+}
+
/// Automation features supported by this host authority.
///
/// The presence of this object advertises the baseline `ahp-automations://`
@@ -462,7 +484,10 @@ public sealed record SubscribeResult
///
/// After creation, the client should subscribe to the session URI to receive state
/// updates. The server also broadcasts a `root/sessionAdded` notification to all
-/// clients.
+/// clients.
+///
+/// Repository preparation MUST finish before `session/ready` or executing turns.
+/// Clients recover the outcome from session state, not progress notifications.
public sealed record CreateSessionParams
{
/// Session URI (client-chosen, e.g. `ahp-session:/<uuid>`)
@@ -490,10 +515,19 @@ public sealed record CreateSessionParams
/// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that
/// capability treats only the first entry as the session's working directory
/// and ignores the rest. Dispatch working-directory actions to change the set
- /// after the session has started.
+ /// after the session has started.
+ ///
+ /// A non-empty list and `repositories` are mutually exclusive.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? WorkingDirectories { get; init; }
+ /// Non-empty repository list to prepare, supported only when the host
+ /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ /// directory/default creation. The resulting working directories MUST fit
+ /// the selected agent's existing directory capabilities.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -1346,7 +1380,10 @@ public sealed record DisposeTerminalParams
/// The client calls this command whenever the user changes a significant input
/// (e.g. picks a working directory, toggles a property). Each response returns
/// the full current property set (not a delta). The returned `values` contain
-/// server-resolved defaults to pass to `createSession`.
+/// server-resolved defaults to pass to `createSession`.
+///
+/// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or
+/// prepare repositories: editing a draft should not create checkouts.
public sealed record ResolveSessionConfigParams
{
public required string Channel { get; init; }
@@ -1365,6 +1402,11 @@ public sealed record ResolveSessionConfigParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkingDirectory { get; init; }
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Current user-filled configuration values
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Config { get; init; }
@@ -1403,6 +1445,11 @@ public sealed record SessionConfigCompletionsParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkingDirectory { get; init; }
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Current user-filled configuration values (provides context for the query)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Config { get; init; }
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index 44ceb2ab..041917c4 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -264,6 +264,8 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(ReconnectResult))]
[JsonSerializable(typeof(ReconnectResultType))]
[JsonSerializable(typeof(ReconnectSnapshotResult))]
+[JsonSerializable(typeof(RepositoryPreparationCapabilities))]
+[JsonSerializable(typeof(RepositorySource))]
[JsonSerializable(typeof(ResolveSessionConfigParams))]
[JsonSerializable(typeof(ResolveSessionConfigResult))]
[JsonSerializable(typeof(ResourceChange))]
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
index 950feef8..2640d70f 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
@@ -267,6 +267,13 @@ public sealed record PartialSessionSummary
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? WorkingDirectories { get; init; }
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 8ade9986..d5015bee 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -940,6 +940,19 @@ public sealed record MultipleWorkingDirectoriesCapability
public bool? PrimaryReplacement { get; init; }
}
+/// Requested repository intent, independent of any host-resolved checkout.
+/// The same source may appear more than once with different revisions; a source
+/// URI is not a checkout identity.
+public sealed record RepositorySource
+{
+ /// Credential-free repository source URI.
+ public required string Source { get; init; }
+
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Revision { get; init; }
+}
+
public sealed record SessionModelInfo
{
/// Model identifier
@@ -1547,6 +1560,13 @@ public sealed class SessionState
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? WorkingDirectories { get; set; }
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; set; }
+
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -1882,6 +1902,13 @@ public sealed class SessionSummary
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? WorkingDirectories { get; set; }
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; set; }
+
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
index d0fa9c1d..743d1f78 100644
--- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
+++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
@@ -164,6 +164,12 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type,
return Wrap(Ser.Deserialize(inputJson));
case "InitializeResult":
return Wrap(Ser.Deserialize(inputJson));
+ case "CreateSessionParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "ResolveSessionConfigParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "SessionConfigCompletionsParams":
+ return Wrap(Ser.Deserialize(inputJson));
case "Snapshot":
return Wrap(Ser.Deserialize(inputJson));
default:
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 9ef81b6f..d48d6625 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -160,6 +160,10 @@ type InitializeResult struct {
Snapshots []Snapshot `json:"snapshots"`
// Suggested default directory for remote filesystem browsing
DefaultDirectory *URI `json:"defaultDirectory,omitempty"`
+ // Host-owned repository preparation for session creation and repository
+ // context in configuration queries. Absence means unsupported; an empty
+ // object supports one repository at its default revision.
+ RepositoryPreparation *RepositoryPreparationCapabilities `json:"repositoryPreparation,omitempty"`
// Characters that, when typed in a {@link Message} input, SHOULD cause
// the client to issue a `completions` request with
// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -182,6 +186,18 @@ type InitializeResult struct {
Automations *AutomationCapabilities `json:"automations,omitempty"`
}
+// Repository preparation supported by this host, independent of the selected
+// agent. Resulting working directories must still fit that agent's existing
+// directory capabilities.
+type RepositoryPreparationCapabilities struct {
+ // When true, clients may supply {@link RepositorySource.revision}.
+ Revision *bool `json:"revision,omitempty"`
+ // When true, clients may supply more than one repository. When absent or
+ // false, the host MUST reject lists with more than one entry with
+ // `InvalidParams` before preparation.
+ MultipleRepositories *bool `json:"multipleRepositories,omitempty"`
+}
+
// Optional capabilities a client declares during `initialize`.
//
// Each field is a presence flag: an empty object `{}` means "supported",
@@ -375,6 +391,9 @@ type SubscribeResult struct {
// After creation, the client should subscribe to the session URI to receive state
// updates. The server also broadcasts a `root/sessionAdded` notification to all
// clients.
+//
+// Repository preparation MUST finish before `session/ready` or executing turns.
+// Clients recover the outcome from session state, not progress notifications.
type CreateSessionParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
@@ -396,7 +415,14 @@ type CreateSessionParams struct {
// capability treats only the first entry as the session's working directory
// and ignores the rest. Dispatch working-directory actions to change the set
// after the session has started.
+ //
+ // A non-empty list and `repositories` are mutually exclusive.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Non-empty repository list to prepare, supported only when the host
+ // advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ // directory/default creation. The resulting working directories MUST fit
+ // the selected agent's existing directory capabilities.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Agent-specific configuration values collected via `resolveSessionConfig`.
// Keys and values correspond to the schema returned by the server.
Config map[string]json.RawMessage `json:"config,omitempty"`
@@ -1070,6 +1096,9 @@ type DisposeTerminalParams struct {
// (e.g. picks a working directory, toggles a property). Each response returns
// the full current property set (not a delta). The returned `values` contain
// server-resolved defaults to pass to `createSession`.
+//
+// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or
+// prepare repositories: editing a draft should not create checkouts.
type ResolveSessionConfigParams struct {
// Channel URI this command targets.
Channel URI `json:"channel"`
@@ -1080,6 +1109,9 @@ type ResolveSessionConfigParams struct {
Provider *string `json:"provider,omitempty"`
// Working directory for the session
WorkingDirectory *URI `json:"workingDirectory,omitempty"`
+ // Non-empty repository context, subject to
+ // {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Current user-filled configuration values
Config map[string]json.RawMessage `json:"config,omitempty"`
}
@@ -1107,6 +1139,9 @@ type SessionConfigCompletionsParams struct {
Provider *string `json:"provider,omitempty"`
// Working directory for the session
WorkingDirectory *URI `json:"workingDirectory,omitempty"`
+ // Non-empty repository context, subject to
+ // {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Current user-filled configuration values (provides context for the query)
Config map[string]json.RawMessage `json:"config,omitempty"`
// Property id from the schema to query values for
diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go
index 80e77625..7a5f1802 100644
--- a/clients/go/ahptypes/notifications.generated.go
+++ b/clients/go/ahptypes/notifications.generated.go
@@ -232,6 +232,11 @@ type PartialSessionSummary struct {
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Immutable repository intent accepted at creation. When present, this list
+ // is non-empty and retained exactly, including order and omitted revisions,
+ // from `creating` through `ready` or `failed` and in session summaries.
+ // Entries have no one-to-one or positional mapping to `workingDirectories`.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-session://annotations`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go
index 0f718138..4d54bc7f 100644
--- a/clients/go/ahptypes/roundtrip_fixture_test.go
+++ b/clients/go/ahptypes/roundtrip_fixture_test.go
@@ -242,6 +242,18 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
var v InitializeResult
dec(&v)
return enc(&v)
+ case "CreateSessionParams":
+ var v CreateSessionParams
+ dec(&v)
+ return enc(&v)
+ case "ResolveSessionConfigParams":
+ var v ResolveSessionConfigParams
+ dec(&v)
+ return enc(&v)
+ case "SessionConfigCompletionsParams":
+ var v SessionConfigCompletionsParams
+ dec(&v)
+ return enc(&v)
case "ChatSource":
var v ChatSource
dec(&v)
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 5893c2af..ef170c14 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -744,6 +744,16 @@ type MultipleWorkingDirectoriesCapability struct {
PrimaryReplacement *bool `json:"primaryReplacement,omitempty"`
}
+// Requested repository intent, independent of any host-resolved checkout.
+// The same source may appear more than once with different revisions; a source
+// URI is not a checkout identity.
+type RepositorySource struct {
+ // Credential-free repository source URI.
+ Source URI `json:"source"`
+ // Requested branch, tag, or commit. Omit to use the host's default revision.
+ Revision *string `json:"revision,omitempty"`
+}
+
type SessionModelInfo struct {
// Model identifier
Id string `json:"id"`
@@ -877,6 +887,11 @@ type SessionState struct {
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Immutable repository intent accepted at creation. When present, this list
+ // is non-empty and retained exactly, including order and omitted revisions,
+ // from `creating` through `ready` or `failed` and in session summaries.
+ // Entries have no one-to-one or positional mapping to `workingDirectories`.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-session://annotations`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
@@ -1153,6 +1168,11 @@ type SessionSummary struct {
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Immutable repository intent accepted at creation. When present, this list
+ // is non-empty and retained exactly, including order and omitted revisions,
+ // from `creating` through `ready` or `failed` and in session summaries.
+ // Entries have no one-to-one or positional mapping to `workingDirectories`.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-session://annotations`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index 01fb0e15..2c647c8d 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -363,6 +363,12 @@ data class InitializeResult(
* Suggested default directory for remote filesystem browsing
*/
val defaultDirectory: String? = null,
+ /**
+ * Host-owned repository preparation for session creation and repository
+ * context in configuration queries. Absence means unsupported; an empty
+ * object supports one repository at its default revision.
+ */
+ val repositoryPreparation: RepositoryPreparationCapabilities? = null,
/**
* Characters that, when typed in a {@link Message} input, SHOULD cause
* the client to issue a `completions` request with
@@ -393,6 +399,20 @@ data class InitializeResult(
val automations: AutomationCapabilities? = null
)
+@Serializable
+data class RepositoryPreparationCapabilities(
+ /**
+ * When true, clients may supply {@link RepositorySource.revision}.
+ */
+ val revision: Boolean? = null,
+ /**
+ * When true, clients may supply more than one repository. When absent or
+ * false, the host MUST reject lists with more than one entry with
+ * `InvalidParams` before preparation.
+ */
+ val multipleRepositories: Boolean? = null
+)
+
@Serializable
data class ClientCapabilities(
/**
@@ -617,8 +637,17 @@ data class CreateSessionParams(
* capability treats only the first entry as the session's working directory
* and ignores the rest. Dispatch working-directory actions to change the set
* after the session has started.
+ *
+ * A non-empty list and `repositories` are mutually exclusive.
*/
val workingDirectories: List? = null,
+ /**
+ * Non-empty repository list to prepare, supported only when the host
+ * advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ * directory/default creation. The resulting working directories MUST fit
+ * the selected agent's existing directory capabilities.
+ */
+ val repositories: List? = null,
/**
* Agent-specific configuration values collected via `resolveSessionConfig`.
* Keys and values correspond to the schema returned by the server.
@@ -1316,6 +1345,11 @@ data class ResolveSessionConfigParams(
* Working directory for the session
*/
val workingDirectory: String? = null,
+ /**
+ * Non-empty repository context, subject to
+ * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ */
+ val repositories: List? = null,
/**
* Current user-filled configuration values
*/
@@ -1433,6 +1467,11 @@ data class SessionConfigCompletionsParams(
* Working directory for the session
*/
val workingDirectory: String? = null,
+ /**
+ * Non-empty repository context, subject to
+ * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ */
+ val repositories: List? = null,
/**
* Current user-filled configuration values (provides context for the query)
*/
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
index b9f2d4eb..9f45c382 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
@@ -225,6 +225,13 @@ data class PartialSessionSummary(
* chat that sets none operates against this full set.
*/
val workingDirectories: List? = null,
+ /**
+ * Immutable repository intent accepted at creation. When present, this list
+ * is non-empty and retained exactly, including order and omitted revisions,
+ * from `creating` through `ready` or `failed` and in session summaries.
+ * Entries have no one-to-one or positional mapping to `workingDirectories`.
+ */
+ val repositories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-session://annotations`). Surfaced so badge UI can render
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 46e82112..9685baeb 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1415,6 +1415,18 @@ data class MultipleWorkingDirectoriesCapability(
val primaryReplacement: Boolean? = null
)
+@Serializable
+data class RepositorySource(
+ /**
+ * Credential-free repository source URI.
+ */
+ val source: String,
+ /**
+ * Requested branch, tag, or commit. Omit to use the host's default revision.
+ */
+ val revision: String? = null
+)
+
@Serializable
data class SessionModelInfo(
/**
@@ -1759,6 +1771,13 @@ data class SessionState(
* chat that sets none operates against this full set.
*/
val workingDirectories: List? = null,
+ /**
+ * Immutable repository intent accepted at creation. When present, this list
+ * is non-empty and retained exactly, including order and omitted revisions,
+ * from `creating` through `ready` or `failed` and in session summaries.
+ * Entries have no one-to-one or positional mapping to `workingDirectories`.
+ */
+ val repositories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-session://annotations`). Surfaced so badge UI can render
@@ -2040,6 +2059,13 @@ data class SessionSummary(
* chat that sets none operates against this full set.
*/
val workingDirectories: List? = null,
+ /**
+ * Immutable repository intent accepted at creation. When present, this list
+ * is non-empty and retained exactly, including order and omitted revisions,
+ * from `creating` through `ready` or `failed` and in session summaries.
+ * Entries have no one-to-one or positional mapping to `workingDirectories`.
+ */
+ val repositories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-session://annotations`). Surfaced so badge UI can render
diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
index 7af1319f..5c90f100 100644
--- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
+++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
@@ -32,6 +32,7 @@ import com.microsoft.agenthostprotocol.generated.ActionEnvelope
import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget
import com.microsoft.agenthostprotocol.generated.ChatSource
import com.microsoft.agenthostprotocol.generated.Customization
+import com.microsoft.agenthostprotocol.generated.CreateSessionParams
import com.microsoft.agenthostprotocol.generated.Implementation
import com.microsoft.agenthostprotocol.generated.InitializeResult
import com.microsoft.agenthostprotocol.generated.JsonRpcErrorResponse
@@ -39,7 +40,9 @@ import com.microsoft.agenthostprotocol.generated.JsonRpcNotification
import com.microsoft.agenthostprotocol.generated.JsonRpcRequest
import com.microsoft.agenthostprotocol.generated.JsonRpcSuccessResponse
import com.microsoft.agenthostprotocol.generated.PartialSessionSummary
+import com.microsoft.agenthostprotocol.generated.ResolveSessionConfigParams
import com.microsoft.agenthostprotocol.generated.SessionAddedParams
+import com.microsoft.agenthostprotocol.generated.SessionConfigCompletionsParams
import com.microsoft.agenthostprotocol.generated.ChatInputQuestion
import com.microsoft.agenthostprotocol.generated.SessionStatus
import com.microsoft.agenthostprotocol.generated.SessionSummary
@@ -253,6 +256,9 @@ class RoundTripCorpusTest {
"PartialSessionSummary" -> rt(PartialSessionSummary.serializer())
"Implementation" -> rt(Implementation.serializer())
"InitializeResult" -> rt(InitializeResult.serializer())
+ "CreateSessionParams" -> rt(CreateSessionParams.serializer())
+ "ResolveSessionConfigParams" -> rt(ResolveSessionConfigParams.serializer())
+ "SessionConfigCompletionsParams" -> rt(SessionConfigCompletionsParams.serializer())
"ChatSource" -> rt(ChatSource.serializer())
"Snapshot" -> rt(Snapshot.serializer())
else -> fail(
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index e3dfe255..78e3e179 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -17,8 +17,9 @@ use crate::actions::{ActionEnvelope, StateAction};
use crate::state::{
AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate,
AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment,
- ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection,
- Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn,
+ ModelSelection, RepositorySource, SessionActiveClient, SessionConfigSchema, SessionSummary,
+ SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange,
+ Turn,
};
// ─── Enums ────────────────────────────────────────────────────────────
@@ -268,6 +269,11 @@ pub struct InitializeResult {
/// Suggested default directory for remote filesystem browsing
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_directory: Option,
+ /// Host-owned repository preparation for session creation and repository
+ /// context in configuration queries. Absence means unsupported; an empty
+ /// object supports one repository at its default revision.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repository_preparation: Option,
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -294,6 +300,22 @@ pub struct InitializeResult {
pub automations: Option,
}
+/// Repository preparation supported by this host, independent of the selected
+/// agent. Resulting working directories must still fit that agent's existing
+/// directory capabilities.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositoryPreparationCapabilities {
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub revision: Option,
+ /// When true, clients may supply more than one repository. When absent or
+ /// false, the host MUST reject lists with more than one entry with
+ /// `InvalidParams` before preparation.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub multiple_repositories: Option,
+}
+
/// Optional capabilities a client declares during `initialize`.
///
/// Each field is a presence flag: an empty object `{}` means "supported",
@@ -558,6 +580,9 @@ pub struct SubscribeResult {
/// After creation, the client should subscribe to the session URI to receive state
/// updates. The server also broadcasts a `root/sessionAdded` notification to all
/// clients.
+///
+/// Repository preparation MUST finish before `session/ready` or executing turns.
+/// Clients recover the outcome from session state, not progress notifications.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionParams {
@@ -583,8 +608,16 @@ pub struct CreateSessionParams {
/// capability treats only the first entry as the session's working directory
/// and ignores the rest. Dispatch working-directory actions to change the set
/// after the session has started.
+ ///
+ /// A non-empty list and `repositories` are mutually exclusive.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directories: Option>,
+ /// Non-empty repository list to prepare, supported only when the host
+ /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ /// directory/default creation. The resulting working directories MUST fit
+ /// the selected agent's existing directory capabilities.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1374,6 +1407,9 @@ pub struct DisposeTerminalParams {
/// (e.g. picks a working directory, toggles a property). Each response returns
/// the full current property set (not a delta). The returned `values` contain
/// server-resolved defaults to pass to `createSession`.
+///
+/// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or
+/// prepare repositories: editing a draft should not create checkouts.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveSessionConfigParams {
@@ -1389,6 +1425,10 @@ pub struct ResolveSessionConfigParams {
/// Working directory for the session
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option,
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Current user-filled configuration values
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option,
@@ -1424,6 +1464,10 @@ pub struct SessionConfigCompletionsParams {
/// Working directory for the session
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option,
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Current user-filled configuration values (provides context for the query)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option,
diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs
index 098d78a3..a492ad57 100644
--- a/clients/rust/crates/ahp-types/src/notifications.rs
+++ b/clients/rust/crates/ahp-types/src/notifications.rs
@@ -14,8 +14,8 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
#[allow(unused_imports)]
use crate::state::{
AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection,
- ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus,
- SessionSummary,
+ ProjectInfo, ProtectedResourceMetadata, RepositorySource, SessionChatSummary, SessionOrigin,
+ SessionStatus, SessionSummary,
};
// ─── Enums ────────────────────────────────────────────────────────────
@@ -293,6 +293,12 @@ pub struct PartialSessionSummary {
/// chat that sets none operates against this full set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directories: Option>,
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index 49cbd7f8..57e87c0e 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1701,6 +1701,19 @@ pub struct MultipleWorkingDirectoriesCapability {
pub primary_replacement: Option,
}
+/// Requested repository intent, independent of any host-resolved checkout.
+/// The same source may appear more than once with different revisions; a source
+/// URI is not a checkout identity.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositorySource {
+ /// Credential-free repository source URI.
+ pub source: Uri,
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub revision: Option,
+}
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionModelInfo {
@@ -2021,6 +2034,12 @@ pub struct SessionState {
/// chat that sets none operates against this full set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directories: Option>,
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -2357,6 +2376,12 @@ pub struct SessionSummary {
/// chat that sets none operates against this full set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directories: Option>,
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
index 1b81a9ca..999f7097 100644
--- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
+++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
@@ -28,7 +28,10 @@
use ahp_types::{
actions::{ActionEnvelope, StateAction},
- commands::{ChangesetOperationTarget, ChatSource, Implementation, InitializeResult},
+ commands::{
+ ChangesetOperationTarget, ChatSource, CreateSessionParams, Implementation,
+ InitializeResult, ResolveSessionConfigParams, SessionConfigCompletionsParams,
+ },
common::StringOrMarkdown,
messages::JsonRpcMessage,
notifications::{PartialSessionSummary, SessionAddedParams},
@@ -223,6 +226,9 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result<
"PartialSessionSummary" => round_trip!(PartialSessionSummary),
"Implementation" => round_trip!(Implementation),
"InitializeResult" => round_trip!(InitializeResult),
+ "CreateSessionParams" => round_trip!(CreateSessionParams),
+ "ResolveSessionConfigParams" => round_trip!(ResolveSessionConfigParams),
+ "SessionConfigCompletionsParams" => round_trip!(SessionConfigCompletionsParams),
"ChatSource" => round_trip!(ChatSource),
"Snapshot" => round_trip!(Snapshot),
other => Err(format!(
diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs
index 95b02ff4..75dfd108 100644
--- a/clients/rust/crates/ahp/src/reducers.rs
+++ b/clients/rust/crates/ahp/src/reducers.rs
@@ -2174,6 +2174,7 @@ mod tests {
origin: None,
project: None,
working_directories: None,
+ repositories: None,
annotations: None,
lifecycle: SessionLifecycle::Creating,
creation_error: None,
diff --git a/clients/rust/crates/ahp/tests/client_roundtrip.rs b/clients/rust/crates/ahp/tests/client_roundtrip.rs
index 1e49931e..907bb33b 100644
--- a/clients/rust/crates/ahp/tests/client_roundtrip.rs
+++ b/clients/rust/crates/ahp/tests/client_roundtrip.rs
@@ -376,6 +376,7 @@ async fn session_config_completions_send_wrapper_targets_root_channel() {
meta: None,
provider: None,
working_directory: None,
+ repositories: None,
config: None,
property: "baseBranch".into(),
query: Some("ma".into()),
diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs
index a3d458ca..1b9c7ad3 100644
--- a/clients/rust/crates/ahp/tests/hosts.rs
+++ b/clients/rust/crates/ahp/tests/hosts.rs
@@ -1096,6 +1096,7 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S
modified_at: modified,
project: None,
working_directories: None,
+ repositories: None,
changes: None,
annotations: None,
meta: None,
diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs
index aa7341e5..d20c6533 100644
--- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs
+++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs
@@ -55,6 +55,7 @@ fn session_state(title: &str, _resource: &str) -> SessionState {
origin: None,
project: None,
working_directories: None,
+ repositories: None,
annotations: None,
lifecycle: SessionLifecycle::Ready,
creation_error: None,
@@ -476,6 +477,7 @@ fn non_action_event_is_ignored() {
modified_at: "1970-01-01T00:00:00.000Z".into(),
project: None,
working_directories: None,
+ repositories: None,
changes: None,
annotations: None,
meta: None,
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index 9d9ca3e2..9a1bdd7d 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -321,6 +321,10 @@ public struct InitializeResult: Codable, Sendable {
public var snapshots: [Snapshot]
/// Suggested default directory for remote filesystem browsing
public var defaultDirectory: String?
+ /// Host-owned repository preparation for session creation and repository
+ /// context in configuration queries. Absence means unsupported; an empty
+ /// object supports one repository at its default revision.
+ public var repositoryPreparation: RepositoryPreparationCapabilities?
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -349,6 +353,7 @@ public struct InitializeResult: Codable, Sendable {
case meta = "_meta"
case snapshots
case defaultDirectory
+ case repositoryPreparation
case completionTriggerCharacters
case terminalCommandPrefix
case telemetry
@@ -362,6 +367,7 @@ public struct InitializeResult: Codable, Sendable {
meta: [String: AnyCodable]? = nil,
snapshots: [Snapshot],
defaultDirectory: String? = nil,
+ repositoryPreparation: RepositoryPreparationCapabilities? = nil,
completionTriggerCharacters: [String]? = nil,
terminalCommandPrefix: String? = nil,
telemetry: TelemetryCapabilities? = nil,
@@ -373,6 +379,7 @@ public struct InitializeResult: Codable, Sendable {
self.meta = meta
self.snapshots = snapshots
self.defaultDirectory = defaultDirectory
+ self.repositoryPreparation = repositoryPreparation
self.completionTriggerCharacters = completionTriggerCharacters
self.terminalCommandPrefix = terminalCommandPrefix
self.telemetry = telemetry
@@ -427,6 +434,23 @@ public struct AutomationCapabilities: Codable, Sendable {
}
}
+public struct RepositoryPreparationCapabilities: Codable, Sendable {
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ public var revision: Bool?
+ /// When true, clients may supply more than one repository. When absent or
+ /// false, the host MUST reject lists with more than one entry with
+ /// `InvalidParams` before preparation.
+ public var multipleRepositories: Bool?
+
+ public init(
+ revision: Bool? = nil,
+ multipleRepositories: Bool? = nil
+ ) {
+ self.revision = revision
+ self.multipleRepositories = multipleRepositories
+ }
+}
+
public struct AutomationCreateCapability: Codable, Sendable {
public init(
@@ -651,7 +675,14 @@ public struct CreateSessionParams: Codable, Sendable {
/// capability treats only the first entry as the session's working directory
/// and ignores the rest. Dispatch working-directory actions to change the set
/// after the session has started.
+ ///
+ /// A non-empty list and `repositories` are mutually exclusive.
public var workingDirectories: [String]?
+ /// Non-empty repository list to prepare, supported only when the host
+ /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ /// directory/default creation. The resulting working directories MUST fit
+ /// the selected agent's existing directory capabilities.
+ public var repositories: [RepositorySource]?
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
public var config: [String: AnyCodable]?
@@ -679,6 +710,7 @@ public struct CreateSessionParams: Codable, Sendable {
case meta = "_meta"
case provider
case workingDirectories
+ case repositories
case config
case activeClient
case progressToken
@@ -689,6 +721,7 @@ public struct CreateSessionParams: Codable, Sendable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectories: [String]? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil,
activeClient: SessionActiveClient? = nil,
progressToken: String? = nil
@@ -697,6 +730,7 @@ public struct CreateSessionParams: Codable, Sendable {
self.meta = meta
self.provider = provider
self.workingDirectories = workingDirectories
+ self.repositories = repositories
self.config = config
self.activeClient = activeClient
self.progressToken = progressToken
@@ -1596,6 +1630,9 @@ public struct ResolveSessionConfigParams: Codable, Sendable {
public var provider: String?
/// Working directory for the session
public var workingDirectory: String?
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ public var repositories: [RepositorySource]?
/// Current user-filled configuration values
public var config: [String: AnyCodable]?
@@ -1604,6 +1641,7 @@ public struct ResolveSessionConfigParams: Codable, Sendable {
case meta = "_meta"
case provider
case workingDirectory
+ case repositories
case config
}
@@ -1612,12 +1650,14 @@ public struct ResolveSessionConfigParams: Codable, Sendable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectory: String? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil
) {
self.channel = channel
self.meta = meta
self.provider = provider
self.workingDirectory = workingDirectory
+ self.repositories = repositories
self.config = config
}
}
@@ -1749,6 +1789,9 @@ public struct SessionConfigCompletionsParams: Codable, Sendable {
public var provider: String?
/// Working directory for the session
public var workingDirectory: String?
+ /// Non-empty repository context, subject to
+ /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ public var repositories: [RepositorySource]?
/// Current user-filled configuration values (provides context for the query)
public var config: [String: AnyCodable]?
/// Property id from the schema to query values for
@@ -1761,6 +1804,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable {
case meta = "_meta"
case provider
case workingDirectory
+ case repositories
case config
case property
case query
@@ -1771,6 +1815,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectory: String? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil,
property: String,
query: String? = nil
@@ -1779,6 +1824,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable {
self.meta = meta
self.provider = provider
self.workingDirectory = workingDirectory
+ self.repositories = repositories
self.config = config
self.property = property
self.query = query
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
index 4f8f4894..3c3ff33b 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
@@ -217,6 +217,11 @@ public struct PartialSessionSummary: Codable, Sendable {
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
public var workingDirectories: [String]?
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ public var repositories: [RepositorySource]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -255,6 +260,7 @@ public struct PartialSessionSummary: Codable, Sendable {
case origin
case project
case workingDirectories
+ case repositories
case annotations
case resource
case createdAt
@@ -273,6 +279,7 @@ public struct PartialSessionSummary: Codable, Sendable {
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
workingDirectories: [String]? = nil,
+ repositories: [RepositorySource]? = nil,
annotations: AnnotationsSummary? = nil,
resource: String? = nil,
createdAt: String? = nil,
@@ -289,6 +296,7 @@ public struct PartialSessionSummary: Codable, Sendable {
self.origin = origin
self.project = project
self.workingDirectories = workingDirectories
+ self.repositories = repositories
self.annotations = annotations
self.resource = resource
self.createdAt = createdAt
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index ec02a9bb..6660c0e3 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1411,6 +1411,21 @@ public struct MultipleWorkingDirectoriesCapability: Codable, Sendable {
}
}
+public struct RepositorySource: Codable, Sendable {
+ /// Credential-free repository source URI.
+ public var source: String
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ public var revision: String?
+
+ public init(
+ source: String,
+ revision: String? = nil
+ ) {
+ self.source = source
+ self.revision = revision
+ }
+}
+
public struct SessionModelInfo: Codable, Sendable {
/// Model identifier
public var id: String
@@ -1811,6 +1826,11 @@ public struct SessionState: Codable, Sendable {
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
public var workingDirectories: [String]?
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ public var repositories: [RepositorySource]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -1900,6 +1920,7 @@ public struct SessionState: Codable, Sendable {
case origin
case project
case workingDirectories
+ case repositories
case annotations
case lifecycle
case creationError
@@ -1922,6 +1943,7 @@ public struct SessionState: Codable, Sendable {
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
workingDirectories: [String]? = nil,
+ repositories: [RepositorySource]? = nil,
annotations: AnnotationsSummary? = nil,
lifecycle: SessionLifecycle,
creationError: ErrorInfo? = nil,
@@ -1942,6 +1964,7 @@ public struct SessionState: Codable, Sendable {
self.origin = origin
self.project = project
self.workingDirectories = workingDirectories
+ self.repositories = repositories
self.annotations = annotations
self.lifecycle = lifecycle
self.creationError = creationError
@@ -2139,6 +2162,11 @@ public struct SessionSummary: Codable, Sendable {
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
public var workingDirectories: [String]?
+ /// Immutable repository intent accepted at creation. When present, this list
+ /// is non-empty and retained exactly, including order and omitted revisions,
+ /// from `creating` through `ready` or `failed` and in session summaries.
+ /// Entries have no one-to-one or positional mapping to `workingDirectories`.
+ public var repositories: [RepositorySource]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-session://annotations`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -2177,6 +2205,7 @@ public struct SessionSummary: Codable, Sendable {
case origin
case project
case workingDirectories
+ case repositories
case annotations
case resource
case createdAt
@@ -2195,6 +2224,7 @@ public struct SessionSummary: Codable, Sendable {
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
workingDirectories: [String]? = nil,
+ repositories: [RepositorySource]? = nil,
annotations: AnnotationsSummary? = nil,
resource: String,
createdAt: String,
@@ -2211,6 +2241,7 @@ public struct SessionSummary: Codable, Sendable {
self.origin = origin
self.project = project
self.workingDirectories = workingDirectories
+ self.repositories = repositories
self.annotations = annotations
self.resource = resource
self.createdAt = createdAt
diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
index fc3031a3..70cb7999 100644
--- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
+++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
@@ -188,6 +188,12 @@ final class TypesRoundTripFixtureTests: XCTestCase {
return try reencode(dec.decode(Implementation.self, from: inputData))
case "InitializeResult":
return try reencode(dec.decode(InitializeResult.self, from: inputData))
+ case "CreateSessionParams":
+ return try reencode(dec.decode(CreateSessionParams.self, from: inputData))
+ case "ResolveSessionConfigParams":
+ return try reencode(dec.decode(ResolveSessionConfigParams.self, from: inputData))
+ case "SessionConfigCompletionsParams":
+ return try reencode(dec.decode(SessionConfigCompletionsParams.self, from: inputData))
case "ChatSource":
return try reencode(dec.decode(ChatSource.self, from: inputData))
case "Snapshot":
diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts
index 6177c092..7e54b2fe 100644
--- a/clients/typescript/test/client.test.ts
+++ b/clients/typescript/test/client.test.ts
@@ -42,6 +42,12 @@ import type {
import { JsonRpcErrorCodes } from '../src/types/common/errors.js';
import { AutomationOperation, type AutomationEntry } from '../src/types/channels-automation/state.js';
import { MessageKind } from '../src/types/channels-chat/state.js';
+import {
+ SessionLifecycle,
+ SessionStatus,
+ type SessionConfigSchema,
+ type SessionState,
+} from '../src/types/index.js';
const ROOT = 'ahp-root://' as const;
const AUTOMATIONS = 'ahp-automations://' as const;
@@ -108,6 +114,178 @@ test('initialize round-trip', async () => {
await client.shutdown();
});
+for (const repositoryPreparation of [
+ undefined, {}, { revision: true }, { multipleRepositories: false },
+ { multipleRepositories: true }, { revision: true, multipleRepositories: true },
+]) {
+ test(`initialize preserves host repository capability ${JSON.stringify(repositoryPreparation)}`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const initialization = client.initialize({ clientId: 'repository-client', protocolVersions: ['0.9.0'] });
+ const request = await readRequest(s);
+ const result: InitializeResult = {
+ protocolVersion: '0.9.0', serverSeq: 0, snapshots: [],
+ ...(repositoryPreparation === undefined ? {} : { repositoryPreparation }),
+ };
+ reply(s, request.id, result);
+ assert.deepEqual(await initialization, result);
+ });
+}
+
+const repositorySource = 'https://example.org/team/project.git';
+for (const { name, repositories } of [
+ { name: 'default revision', repositories: [{ source: repositorySource }] },
+ { name: 'explicit revision', repositories: [{ source: repositorySource, revision: 'refs/tags/v1.2.3' }] },
+ { name: 'same source at two revisions', repositories: [
+ { source: repositorySource, revision: 'main' },
+ { source: repositorySource, revision: 'feature' },
+ ] },
+]) {
+ test(`typed requests round-trip repository lists (${name}) outside config`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const schema: SessionConfigSchema = {
+ type: 'object',
+ properties: {
+ mode: { type: 'string', title: 'Mode', default: 'review' },
+ },
+ };
+ const discovery = client.request('resolveSessionConfig', { channel: ROOT });
+ const discoveryRequest = await readRequest(s);
+ assert.equal(discoveryRequest.method, 'resolveSessionConfig');
+ assert.deepEqual(discoveryRequest.params, { channel: ROOT });
+ reply(s, discoveryRequest.id, { schema, values: { mode: 'review' } });
+
+ const discovered = await discovery;
+ assert.deepEqual(discovered.schema, schema);
+ const context = { repositories, workingDirectory: 'file:///work/context' };
+ const config = discovered.values;
+ const resolution = client.request('resolveSessionConfig', { channel: ROOT, ...context, config });
+ const resolveRequest = await readRequest(s);
+ assert.equal(resolveRequest.method, 'resolveSessionConfig');
+ assert.deepEqual(resolveRequest.params, { channel: ROOT, ...context, config });
+ reply(s, resolveRequest.id, { schema, values: config });
+ const resolved = await resolution;
+ assert.deepEqual(resolved.values, config);
+
+ const completions = client.request('sessionConfigCompletions', { channel: ROOT, ...context, config: resolved.values, property: 'mode' });
+ const completionRequest = await readRequest(s);
+ assert.deepEqual(completionRequest.params, { channel: ROOT, ...context, config: resolved.values, property: 'mode' });
+ reply(s, completionRequest.id, { items: [] });
+ await completions;
+
+ const params = { channel: 'ahp-session:/repository-test', repositories, config: resolved.values };
+ const creation = client.request('createSession', params);
+ const createRequest = await readRequest(s);
+ assert.equal(createRequest.method, 'createSession');
+ assert.deepEqual(createRequest.params, params);
+ reply(s, createRequest.id, null);
+ assert.equal(await creation, null);
+ });
+}
+
+for (const method of ['resolveSessionConfig', 'sessionConfigCompletions', 'createSession'] as const) {
+ test(`${method} surfaces host rejection of repository intent`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const channel = method === 'createSession' ? 'ahp-session:/repository-test' : ROOT;
+ const params = {
+ channel,
+ repositories: [{ source: repositorySource, revision: 'unsupported' }],
+ ...(method === 'sessionConfigCompletions' ? { property: 'mode' } : {}),
+ };
+ const request = method === 'sessionConfigCompletions'
+ ? client.request(method, { ...params, property: 'mode' })
+ : client.request(method, params);
+ const rejected = assert.rejects(request, new RpcError(JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision'));
+ const sent = await readRequest(s);
+ assert.deepEqual({ method: sent.method, params: sent.params }, { method, params });
+ replyError(s, sent.id, JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision');
+ await rejected;
+ });
+}
+
+for (const failed of [false, true]) {
+ test(`session state recovers repository intent and directories after creation ${failed ? 'fails' : 'succeeds'}`, () => {
+ const resource = 'ahp-session:/repository-test';
+ const initial: SessionState = {
+ provider: 'example',
+ title: 'Repository session',
+ status: SessionStatus.Idle,
+ lifecycle: SessionLifecycle.Creating,
+ activeClients: [],
+ chats: [],
+ workingDirectories: [],
+ repositories: [
+ { source: repositorySource },
+ { source: repositorySource, revision: 'main' },
+ { source: repositorySource, revision: 'feature' },
+ ],
+ config: {
+ schema: { type: 'object', properties: { mode: { type: 'string', title: 'Mode' } } },
+ values: { mode: 'review' },
+ },
+ };
+ const mirror = new AhpStateMirror();
+ mirror.applySnapshot({ resource, state: initial, fromSeq: 0 });
+ mirror.apply({
+ channel: resource,
+ serverSeq: 1,
+ origin: undefined,
+ action: { type: ActionType.SessionWorkingDirectorySet, directory: 'file:///work/project' },
+ });
+ mirror.apply({
+ channel: resource,
+ serverSeq: 2,
+ origin: undefined,
+ action: { type: ActionType.SessionWorkingDirectorySet, directory: 'file:///work/project-worktree' },
+ });
+ const preparing = mirror.getSession(resource);
+ assert.ok(preparing);
+ assert.equal(preparing.lifecycle, SessionLifecycle.Creating);
+ assert.deepEqual(preparing.config, initial.config);
+ assert.deepEqual(preparing.repositories, initial.repositories);
+
+ const joining = new AhpStateMirror();
+ joining.applySnapshot({ resource, state: preparing, fromSeq: 2 });
+ assert.deepEqual(joining.getSession(resource), preparing);
+
+ const completion: ActionEnvelope = {
+ channel: resource,
+ serverSeq: 3,
+ origin: undefined,
+ action: failed
+ ? { type: ActionType.SessionCreationFailed, error: { errorType: 'preparationFailed', message: 'Preparation failed' } }
+ : { type: ActionType.SessionReady },
+ };
+ mirror.apply(completion);
+ joining.apply(completion);
+ const completed = mirror.getSession(resource);
+ assert.ok(completed);
+ assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready);
+ assert.deepEqual(completed.config, initial.config);
+ assert.deepEqual(completed.repositories, initial.repositories);
+ assert.deepEqual(completed.workingDirectories, ['file:///work/project', 'file:///work/project-worktree']);
+ assert.deepEqual(joining.getSession(resource), completed);
+ if (failed) {
+ assert.deepEqual(completed.creationError, { errorType: 'preparationFailed', message: 'Preparation failed' });
+ }
+
+ const reconnected = new AhpStateMirror();
+ reconnected.applySnapshot({ resource, state: completed, fromSeq: 3 });
+ assert.deepEqual(reconnected.getSession(resource), completed);
+ });
+}
+
test('subscribe attaches before sending the request and fans out an action', async () => {
const [c, s] = InMemoryTransport.pair();
const client = new AhpClient(c);
diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts
index f5736a2b..9a4d84eb 100644
--- a/clients/typescript/test/types-round-trip.test.ts
+++ b/clients/typescript/test/types-round-trip.test.ts
@@ -61,6 +61,8 @@ import type {
import type { SessionAddedParams } from '../src/types/channels-root/notifications.js';
import type { Implementation, InitializeResult } from '../src/types/common/commands.js';
import type { ChatSource } from '../src/types/channels-chat/commands.js';
+import type { CreateSessionParams } from '../src/types/channels-session/commands.js';
+import type { ResolveSessionConfigParams, SessionConfigCompletionsParams } from '../src/types/channels-root/commands.js';
// ─── Fixture directory ───────────────────────────────────────────────────────
@@ -243,6 +245,9 @@ function bindToType(file: string, type: string, parsed: unknown): void {
case 'PartialSessionSummary': void (parsed as Partial); break;
case 'Implementation': void (parsed as Implementation); break;
case 'InitializeResult': void (parsed as InitializeResult); break;
+ case 'CreateSessionParams': void (parsed as CreateSessionParams); break;
+ case 'ResolveSessionConfigParams': void (parsed as ResolveSessionConfigParams); break;
+ case 'SessionConfigCompletionsParams': void (parsed as SessionConfigCompletionsParams); break;
case 'ChatSource': void (parsed as ChatSource); break;
case 'Snapshot': void (parsed as Snapshot); break;
default:
diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json
new file mode 100644
index 00000000..acf6cd7f
--- /dev/null
+++ b/docs/.changes/20260915-repository-session-config.json
@@ -0,0 +1,4 @@
+{
+ "type": "added",
+ "message": "Typed optional `repositories` lists on session creation, configuration queries, and immutable session metadata, with host-level `repositoryPreparation` discovery."
+}
diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md
index 4066ad14..9880a7d0 100644
--- a/docs/specification/session-channel.md
+++ b/docs/specification/session-channel.md
@@ -35,6 +35,90 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot
[`createSession`](/reference/session#createsession) is a JSON-RPC request. The client picks the URI; the server allocates session state and begins backend initialisation. If the URI is already in use the server returns `SessionAlreadyExists` (`-32003`).
+#### Repository-backed creation
+
+A host can offer to prepare repositories for a new session through a typed list. The client collects repository intent; the host owns authorization, credentials, and preparation. The baseline capability supports **one repository**; a separate option allows future hosts to support multiple repositories without changing the request shape. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command.
+
+##### Capability and field constraints
+
+The **host**, not an individual agent, opts in through [`InitializeResult.repositoryPreparation`](/reference/common#initialize):
+
+| Capability | Meaning |
+|---|---|
+| Absent | Repository preparation and repository context in configuration queries are unsupported. |
+| `{}` | Supports one repository at its default revision. |
+| `revision: true` | Also supports an explicit branch, tag, or commit. |
+| `multipleRepositories: true` | Also supports a list containing more than one repository. Absent or `false` means exactly one entry when a list is present. |
+
+The optional typed `repositories: RepositorySource[]` field is shared by [`CreateSessionParams`](/reference/session#createsessionparams), [`ResolveSessionConfigParams`](/reference/root#resolvesessionconfigparams), and [`SessionConfigCompletionsParams`](/reference/root#sessionconfigcompletionsparams). Each [`RepositorySource`](/reference/session#repositorysource) contains:
+
+| Field | Meaning |
+|---|---|
+| `source` | Required credential-free repository URI identifying the requested source. |
+| `revision` | Optional branch, tag, or commit. Omission requests the host's default revision. |
+
+Clients MUST check the host capability rather than infer support from a provider name, protocol version, `_meta`, or configuration property. A host MUST NOT accept repository input without the capability, or an explicit revision unless `revision` is `true`. When `multipleRepositories` is absent or `false`, it MUST reject lists containing more than one entry **before any preparation**, rather than preparing only the first entry.
+
+When present, `repositories` MUST be non-empty. An absent list retains existing directory/default creation; advertising support does not make the field required. The generated types and schemas describe the list and entry shape; the host enforces capability, authorization, URI validity, and cross-field constraints. Only the typed `repositories` field carries repository intent. Generic `config` and its schema remain unchanged, with no aliases or repository-specific configuration carrier.
+
+##### Values and validation
+
+For example, the initialization result can advertise single-repository preparation with revision selection:
+
+```json
+{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true }
+}
+```
+
+The client passes the same list as context when resolving configuration or requesting completions, then sends it beside the returned configuration when creating the session:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "createSession",
+ "params": {
+ "channel": "ahp-session:/new-session",
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ],
+ "config": { "mode": "interactive" }
+ }
+}
+```
+
+The repository URI identifies a source, not a checkout or host filesystem directory. There is **no one-to-one or positional mapping** between `repositories` and the resolved `workingDirectories`. One source can produce multiple directories, and a multi-repository host may accept the same source at different revisions. Clients MUST NOT deduplicate entries by source URI or use it as a checkout identity. No per-repository IDs are introduced.
+
+`createSession.repositories` and a non-empty `createSession.workingDirectories` list are mutually exclusive. Configuration queries may include `repositories` together with an existing `workingDirectory` as context; preparation belongs to creation, not draft configuration. The resulting directories MUST fit the selected provider's existing directory capabilities. Host-level repository support does not grant an agent support for multiple working directories.
+
+Each entry MUST contain a non-empty `source`, and a supplied `revision` MUST be a non-empty string. A revision without a source is invalid. For creation and configuration queries, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an empty list, an unsupported list length or revision, a malformed or credential-bearing source URI, or conflicting creation directories. It MUST NOT silently drop entries, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent.
+
+Repository source URIs MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs.
+
+##### Preparation and recoverable state
+
+Repository preparation is part of the existing `creating` lifecycle. The host MUST finish preparation before executing turns or publishing `session/ready`. No additional lifecycle state is introduced.
+
+The host MUST publish the accepted `repositories` list in the initial `creating` snapshot and retain it **exactly**, including entry order and omitted revisions, through `ready` or `failed`. This immutable field belongs to [`SessionMetadata`](/reference/session#sessionmetadata), so summaries carry the same list. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. Configuration and working-directory actions do not change this list.
+
+Before dispatching `session/ready` or `session/creationFailed`, the host MUST publish the actual resolved `workingDirectories` in session state, using the existing snapshot and working-directory actions. While no directory has been resolved, `workingDirectories` MAY be absent or empty; do not claim a checkout was prepared when preparation failed. On failure, the existing `session/creationFailed` action records `lifecycle: "failed"` and `creationError`. Both outcomes retain the requested intent and any resolved directories so clients can recover them from a snapshot or replay.
+
+The host MAY report preparation through the existing `createSession.progressToken` and [`root/progress`](./root-channel#progress). Progress is optional, ephemeral, and not replayed. Neither a completed progress indicator nor a successful command response is a replacement for session readiness or failure state.
+
+##### Reattachment, retry, and cleanup
+
+`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that its complete `repositories` list matches the requested intent, including order and revision omission, and inspect the lifecycle. A mismatch is a conflict, not successful recovery. A duplicate creation error alone is not successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session; no per-repository retry is introduced.
+
+The host owns the lifetime of preparation resources it creates; a source URI does not establish ownership of an existing or shared checkout. This capability adds no cancellation RPC or new disposal rules.
+
+##### Minimal-client behavior
+
+A client supporting this capability collects a repository list separately from configuration and respects the host's single- or multi-repository limit. It needs no Git implementation, clone RPC, or progress implementation. Minimal clients can omit `repositories` and continue using directory/default creation. Joining or reconnecting clients read the list, lifecycle, and working directories from authoritative session state without repeating preparation.
+
### Active session
Once a session reaches `lifecycle: 'ready'`, clients may create chats on it with [`createChat`](/reference/chat#createchat). Each chat is independently subscribable at its own `ahp-chat:/` URI; see the [Chat Channel specification](./chat-channel) for the per-chat lifecycle, turn flow, tool calls, and input request handling.
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index ebb9b800..6a2ad492 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -3093,6 +3093,23 @@
"run"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -3128,6 +3145,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -3174,6 +3199,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -3482,6 +3515,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index 659be47c..dccf1a48 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -163,6 +163,10 @@
"$ref": "#/$defs/URI",
"description": "Suggested default directory for remote filesystem browsing"
},
+ "repositoryPreparation": {
+ "$ref": "#/$defs/RepositoryPreparationCapabilities",
+ "description": "Host-owned repository preparation for session creation and repository\ncontext in configuration queries. Absence means unsupported; an empty\nobject supports one repository at its default revision."
+ },
"completionTriggerCharacters": {
"type": "array",
"items": {
@@ -189,6 +193,20 @@
"snapshots"
]
},
+ "RepositoryPreparationCapabilities": {
+ "type": "object",
+ "description": "Repository preparation supported by this host, independent of the selected\nagent. Resulting working directories must still fit that agent's existing\ndirectory capabilities.",
+ "properties": {
+ "revision": {
+ "type": "boolean",
+ "description": "When true, clients may supply {@link RepositorySource.revision}."
+ },
+ "multipleRepositories": {
+ "type": "boolean",
+ "description": "When true, clients may supply more than one repository. When absent or\nfalse, the host MUST reject lists with more than one entry with\n`InvalidParams` before preparation."
+ }
+ }
+ },
"AutomationCapabilities": {
"type": "object",
"description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.",
@@ -949,7 +967,7 @@
},
"ResolveSessionConfigParams": {
"type": "object",
- "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.",
+ "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\n`resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or\nprepare repositories: editing a draft should not create checkouts.",
"properties": {
"channel": {
"type": "string",
@@ -970,6 +988,14 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.",
+ "minItems": 1
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -1044,6 +1070,14 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.",
+ "minItems": 1
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -1081,7 +1115,7 @@
},
"CreateSessionParams": {
"type": "object",
- "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.",
+ "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nRepository preparation MUST finish before `session/ready` or executing turns.\nClients recover the outcome from session state, not progress notifications.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -1101,7 +1135,15 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started."
+ "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositories` are mutually exclusive."
+ },
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository list to prepare, supported only when the host\nadvertises {@link InitializeResult.repositoryPreparation}. Omit to retain\ndirectory/default creation. The resulting working directories MUST fit\nthe selected agent's existing directory capabilities.",
+ "minItems": 1
},
"config": {
"type": "object",
@@ -2337,6 +2379,23 @@
"run"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -2372,6 +2431,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -2418,6 +2485,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -2726,6 +2801,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 132ee4f5..7a47919a 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -766,6 +766,23 @@
"run"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -801,6 +818,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -847,6 +872,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -1155,6 +1188,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -5852,6 +5893,10 @@
"$ref": "#/$defs/URI",
"description": "Suggested default directory for remote filesystem browsing"
},
+ "repositoryPreparation": {
+ "$ref": "#/$defs/RepositoryPreparationCapabilities",
+ "description": "Host-owned repository preparation for session creation and repository\ncontext in configuration queries. Absence means unsupported; an empty\nobject supports one repository at its default revision."
+ },
"completionTriggerCharacters": {
"type": "array",
"items": {
@@ -5878,6 +5923,20 @@
"snapshots"
]
},
+ "RepositoryPreparationCapabilities": {
+ "type": "object",
+ "description": "Repository preparation supported by this host, independent of the selected\nagent. Resulting working directories must still fit that agent's existing\ndirectory capabilities.",
+ "properties": {
+ "revision": {
+ "type": "boolean",
+ "description": "When true, clients may supply {@link RepositorySource.revision}."
+ },
+ "multipleRepositories": {
+ "type": "boolean",
+ "description": "When true, clients may supply more than one repository. When absent or\nfalse, the host MUST reject lists with more than one entry with\n`InvalidParams` before preparation."
+ }
+ }
+ },
"AutomationCapabilities": {
"type": "object",
"description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.",
@@ -6638,7 +6697,7 @@
},
"ResolveSessionConfigParams": {
"type": "object",
- "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.",
+ "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\n`resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or\nprepare repositories: editing a draft should not create checkouts.",
"properties": {
"channel": {
"type": "string",
@@ -6659,6 +6718,14 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.",
+ "minItems": 1
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -6733,6 +6800,14 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.",
+ "minItems": 1
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -6770,7 +6845,7 @@
},
"CreateSessionParams": {
"type": "object",
- "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.",
+ "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nRepository preparation MUST finish before `session/ready` or executing turns.\nClients recover the outcome from session state, not progress notifications.",
"properties": {
"channel": {
"$ref": "#/$defs/URI",
@@ -6790,7 +6865,15 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started."
+ "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositories` are mutually exclusive."
+ },
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Non-empty repository list to prepare, supported only when the host\nadvertises {@link InitializeResult.repositoryPreparation}. Omit to retain\ndirectory/default creation. The resulting working directories MUST fit\nthe selected agent's existing directory capabilities.",
+ "minItems": 1
},
"config": {
"type": "object",
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index d6a65f50..86838bdf 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -109,6 +109,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -944,6 +952,23 @@
"run"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -979,6 +1004,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -1025,6 +1058,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -1333,6 +1374,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
diff --git a/schema/state.schema.json b/schema/state.schema.json
index 47668507..cff008d2 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -677,6 +677,23 @@
"run"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -712,6 +729,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -758,6 +783,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
@@ -1066,6 +1099,14 @@
},
"description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.",
+ "minItems": 1
+ },
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
"description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel."
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index 2946b1aa..ea2ad96c 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -659,6 +659,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'RepositorySource' },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -2081,6 +2082,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str
// record or those fields reference a non-existent type (CS0246).
{ name: 'Implementation' },
{ name: 'ClientCapabilities' },
+ { name: 'RepositoryPreparationCapabilities' },
{ name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index 632ffff0..638e3cba 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -739,6 +739,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'RepositorySource' },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -1716,6 +1717,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding
const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [
{ name: 'InitializeParams' }, { name: 'InitializeResult' },
+ { name: 'RepositoryPreparationCapabilities' },
{ name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts
index 53985809..bfd98012 100644
--- a/scripts/generate-json-schema.test.ts
+++ b/scripts/generate-json-schema.test.ts
@@ -142,7 +142,9 @@ function schemaAccepts(
case 'null':
return value === null;
case 'array':
- return Array.isArray(value);
+ return Array.isArray(value)
+ && (typeof schema.minItems !== 'number' || value.length >= schema.minItems)
+ && value.every(item => schemaAccepts(root, schema.items as JsonNode, item));
}
return true;
@@ -200,6 +202,120 @@ describe('generated JSON schemas', () => {
assert.match(expiresIn.description as string, /MUST be a positive integer/);
});
+ it('keeps session config schema generic without repository metadata', () => {
+ const defs = schema.$defs as Record>;
+ const configSchema = defs.SessionConfigSchema;
+ const properties = configSchema.properties as Record>;
+ assert.deepEqual({
+ fields: Object.keys(properties).sort(),
+ required: configSchema.required,
+ propertySchema: properties.properties.additionalProperties,
+ repositoryType: defs.RepositorySessionConfig,
+ }, {
+ fields: ['properties', 'required', 'type'],
+ required: ['type', 'properties'],
+ propertySchema: { $ref: '#/$defs/SessionConfigPropertySchema' },
+ repositoryType: undefined,
+ });
+
+ const legacy = {
+ type: 'object',
+ properties: { mode: { type: 'string', title: 'Mode' } },
+ required: ['mode'],
+ };
+ assert.equal(schemaAccepts(schema, configSchema, legacy), true);
+ });
+
+ it('declares optional non-empty repository lists beside generic config', () => {
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const defs = schema.$defs as Record>;
+ for (const [definition, channel] of [
+ ['ResolveSessionConfigParams', 'ahp-root://'],
+ ['SessionConfigCompletionsParams', 'ahp-root://'],
+ ['CreateSessionParams', 'ahp-session:/repository-test'],
+ ]) {
+ const properties = defs[definition].properties as Record>;
+ assert.equal(properties.config.type, 'object');
+ assert.deepEqual(
+ Object.keys(properties).filter(name => name.startsWith('repositor')),
+ ['repositories'],
+ );
+ assert.equal(properties.repositories.minItems, 1);
+ const base = { channel, ...(definition === 'SessionConfigCompletionsParams' ? { property: 'mode' } : {}) };
+ assert.equal(schemaAccepts(schema, defs[definition], base), true);
+ const source = 'https://example.org/team/project.git';
+ for (const repositories of [
+ [{ source }],
+ [{ source, revision: 'refs/tags/v1.2.3' }],
+ [{ source, revision: 'main' }, { source, revision: 'feature' }],
+ ]) {
+ assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories, config: { mode: 'review' } }), true);
+ }
+ for (const repositories of [
+ [], null, {}, source, [null], [source], [{}],
+ [{ revision: 'main' }], [{ source: 42 }], [{ source: null }],
+ [{ source, revision: 42 }],
+ ]) {
+ assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories }), false);
+ }
+ }
+ });
+
+ it('declares immutable repository lists with a required source per entry', () => {
+ const defs = schema.$defs as Record>;
+ for (const name of ['SessionMetadata', 'SessionState', 'SessionSummary']) {
+ const properties = defs[name].properties as Record>;
+ const { type, items, minItems } = properties.repositories;
+ assert.deepEqual({ type, items, minItems }, {
+ type: 'array', items: { $ref: '#/$defs/RepositorySource' }, minItems: 1,
+ });
+ assert.equal((defs[name].required as string[]).includes('repositories'), false);
+ }
+ const source = defs.RepositorySource;
+ const properties = source.properties as Record>;
+ assert.deepEqual({
+ fields: Object.keys(properties),
+ required: source.required,
+ sourceType: dereferenceSchema(schema, properties.source).type,
+ revisionType: properties.revision.type,
+ }, {
+ fields: ['source', 'revision'], required: ['source'], sourceType: 'string', revisionType: 'string',
+ });
+ });
+
+ it('advertises repository preparation on the host, not the agent', () => {
+ const defs = schema.$defs as Record>;
+ const capabilities = defs.AgentCapabilities.properties as Record>;
+ assert.deepEqual({
+ agentSource: capabilities.repositorySource,
+ agentPreparation: capabilities.repositoryPreparation,
+ oldCapability: defs.RepositorySourceCapability,
+ }, {
+ agentSource: undefined, agentPreparation: undefined, oldCapability: undefined,
+ });
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const properties = defs.InitializeResult.properties as Record>;
+ assert.equal(properties.repositoryPreparation.$ref, '#/$defs/RepositoryPreparationCapabilities');
+ const hostCapabilities = defs.RepositoryPreparationCapabilities;
+ assert.equal(hostCapabilities.required, undefined);
+ const base = { protocolVersion: '0.9.0', serverSeq: 0, snapshots: [] };
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, base), true);
+ for (const repositoryPreparation of [
+ {}, { revision: true }, { revision: false },
+ { multipleRepositories: false }, { multipleRepositories: true },
+ { revision: true, multipleRepositories: true },
+ ]) {
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), true);
+ }
+ for (const repositoryPreparation of [true, null, [], { revision: 'true' }, { multipleRepositories: 2 }]) {
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), false);
+ }
+ });
+
it('constrains every ChatOrigin branch to a distinct kind', () => {
const defs = schema.$defs as Record>;
const chatOrigin = defs.ChatOrigin;
diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts
index 0ac0fb4f..bfcbab79 100644
--- a/scripts/generate-json-schema.ts
+++ b/scripts/generate-json-schema.ts
@@ -33,6 +33,7 @@ interface JsonSchema {
enum?: Array;
const?: string | number | boolean;
minimum?: number;
+ minItems?: number;
oneOf?: JsonSchema[];
allOf?: JsonSchema[];
anyOf?: JsonSchema[];
@@ -376,6 +377,15 @@ function interfaceToSchema(iface: InterfaceDeclaration, project: Project): JsonS
}
propSchema.minimum = minimum;
}
+ const minItems = getNumericPropertyTag(prop, 'minItems');
+ if (minItems !== undefined) {
+ if (propSchema.type !== 'array' || !Number.isInteger(minItems) || minItems < 0) {
+ throw new Error(
+ `${prop.getSourceFile().getFilePath()}: ${name} uses invalid @minItems on ${typeText}`,
+ );
+ }
+ propSchema.minItems = minItems;
+ }
schema.properties![name] = propSchema;
if (!prop.hasQuestionToken() && !typeAdmitsUndefined(typeText)) {
if (!schema.required!.includes(name)) {
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index 8bce96e8..b35ef777 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -987,6 +987,7 @@ const STATE_STRUCTS = [
'AgentCapabilities',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
+ 'RepositorySource',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest',
@@ -1721,6 +1722,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding
const COMMAND_STRUCTS = [
'InitializeParams', 'InitializeResult',
+ 'RepositoryPreparationCapabilities',
'ClientCapabilities', 'AutomationCapabilities',
'AutomationCreateCapability',
'AutomationScheduleCapabilities',
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index 8585afbb..3518ad45 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -800,6 +800,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'RepositorySource' },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -1699,6 +1700,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding
const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [
{ name: 'InitializeParams' }, { name: 'InitializeResult' },
+ { name: 'RepositoryPreparationCapabilities' },
{ name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
@@ -1764,7 +1766,7 @@ function generateCommandsFile(project: Project): string {
lines.push('#[allow(unused_imports)]');
lines.push('use crate::actions::{ActionEnvelope, StateAction};');
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
+ lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, RepositorySource, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};');
lines.push('');
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
@@ -1919,7 +1921,7 @@ const NOTIFICATION_STRUCTS = [
function generateNotificationsFile(project: Project): string {
const lines: string[] = [GENERATED_HEADER];
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary};');
+ lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, RepositorySource, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary};');
lines.push('');
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 1b046e85..a12fca80 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -692,6 +692,7 @@ const STATE_STRUCTS = [
'AgentCapabilities',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
+ 'RepositorySource',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest',
@@ -1628,6 +1629,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding
const COMMAND_STRUCTS = [
'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'AutomationCapabilities',
+ 'RepositoryPreparationCapabilities',
'AutomationCreateCapability',
'AutomationScheduleCapabilities',
'AutomationRunCancellationCapability',
diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts
index ff3c706b..e7cfc8fb 100644
--- a/types/channels-root/commands.ts
+++ b/types/channels-root/commands.ts
@@ -8,7 +8,7 @@
import type { URI } from '../common/state.js';
import type { BaseParams, PaginatedParams, PaginatedResult } from '../common/commands.js';
-import type { SessionSummary, SessionConfigSchema } from '../channels-session/state.js';
+import type { RepositorySource, SessionSummary, SessionConfigSchema } from '../channels-session/state.js';
// Re-export schema types so the legacy `commands.ts` aggregator continues to
// expose them from the same import path.
@@ -79,6 +79,9 @@ export interface ListSessionsResult extends PaginatedResult {
* the full current property set (not a delta). The returned `values` contain
* server-resolved defaults to pass to `createSession`.
*
+ * `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or
+ * prepare repositories: editing a draft should not create checkouts.
+ *
* @category Commands
* @method resolveSessionConfig
* @direction Client → Server
@@ -130,6 +133,13 @@ export interface ResolveSessionConfigParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
+ /**
+ * Non-empty repository context, subject to
+ * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ *
+ * @minItems 1
+ */
+ repositories?: RepositorySource[];
/** Current user-filled configuration values */
config?: Record;
}
@@ -195,6 +205,13 @@ export interface SessionConfigCompletionsParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
+ /**
+ * Non-empty repository context, subject to
+ * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.
+ *
+ * @minItems 1
+ */
+ repositories?: RepositorySource[];
/** Current user-filled configuration values (provides context for the query) */
config?: Record;
/** Property id from the schema to query values for */
diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts
index 5c452067..0c0425e2 100644
--- a/types/channels-session/commands.ts
+++ b/types/channels-session/commands.ts
@@ -8,6 +8,7 @@
import type { URI } from '../common/state.js';
import type { BaseParams } from '../common/commands.js';
import type {
+ RepositorySource,
SessionActiveClient,
} from './state.js';
import type {
@@ -26,6 +27,9 @@ import type {
* updates. The server also broadcasts a `root/sessionAdded` notification to all
* clients.
*
+ * Repository preparation MUST finish before `session/ready` or executing turns.
+ * Clients recover the outcome from session state, not progress notifications.
+ *
* @category Commands
* @method createSession
* @direction Client → Server
@@ -67,8 +71,18 @@ export interface CreateSessionParams extends BaseParams {
* and ignores the rest. Dispatch working-directory actions to change the set
* after the session has started.
*
+ * A non-empty list and `repositories` are mutually exclusive.
*/
workingDirectories?: URI[];
+ /**
+ * Non-empty repository list to prepare, supported only when the host
+ * advertises {@link InitializeResult.repositoryPreparation}. Omit to retain
+ * directory/default creation. The resulting working directories MUST fit
+ * the selected agent's existing directory capabilities.
+ *
+ * @minItems 1
+ */
+ repositories?: RepositorySource[];
/**
* Agent-specific configuration values collected via `resolveSessionConfig`.
* Keys and values correspond to the schema returned by the server.
diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts
index 3d5931f2..de173769 100644
--- a/types/channels-session/state.ts
+++ b/types/channels-session/state.ts
@@ -102,6 +102,20 @@ export interface AutomationSessionOrigin {
*/
export type SessionOrigin = AutomationSessionOrigin;
+/**
+ * Requested repository intent, independent of any host-resolved checkout.
+ * The same source may appear more than once with different revisions; a source
+ * URI is not a checkout identity.
+ *
+ * @category Session State
+ */
+export interface RepositorySource {
+ /** Credential-free repository source URI. */
+ source: URI;
+ /** Requested branch, tag, or commit. Omit to use the host's default revision. */
+ revision?: string;
+}
+
/**
* Metadata shared between the full {@link SessionState} (delivered when a
* client subscribes to a session's URI) and the lightweight
@@ -141,6 +155,15 @@ export interface SessionMetadata {
* chat that sets none operates against this full set.
*/
workingDirectories?: URI[];
+ /**
+ * Immutable repository intent accepted at creation. When present, this list
+ * is non-empty and retained exactly, including order and omitted revisions,
+ * from `creating` through `ready` or `failed` and in session summaries.
+ * Entries have no one-to-one or positional mapping to `workingDirectories`.
+ *
+ * @minItems 1
+ */
+ repositories?: RepositorySource[];
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-session://annotations`). Surfaced so badge UI can render
diff --git a/types/common/commands.ts b/types/common/commands.ts
index d2bff1e8..40fd1efd 100644
--- a/types/common/commands.ts
+++ b/types/common/commands.ts
@@ -261,6 +261,12 @@ export interface InitializeResult {
snapshots: Snapshot[];
/** Suggested default directory for remote filesystem browsing */
defaultDirectory?: URI;
+ /**
+ * Host-owned repository preparation for session creation and repository
+ * context in configuration queries. Absence means unsupported; an empty
+ * object supports one repository at its default revision.
+ */
+ repositoryPreparation?: RepositoryPreparationCapabilities;
/**
* Characters that, when typed in a {@link Message} input, SHOULD cause
* the client to issue a `completions` request with
@@ -295,6 +301,24 @@ export interface InitializeResult {
automations?: AutomationCapabilities;
}
+/**
+ * Repository preparation supported by this host, independent of the selected
+ * agent. Resulting working directories must still fit that agent's existing
+ * directory capabilities.
+ *
+ * @category Commands
+ */
+export interface RepositoryPreparationCapabilities {
+ /** When true, clients may supply {@link RepositorySource.revision}. */
+ revision?: boolean;
+ /**
+ * When true, clients may supply more than one repository. When absent or
+ * false, the host MUST reject lists with more than one entry with
+ * `InvalidParams` before preparation.
+ */
+ multipleRepositories?: boolean;
+}
+
/**
* Automation features supported by this host authority.
*
diff --git a/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json b/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json
new file mode 100644
index 00000000..5600c247
--- /dev/null
+++ b/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json
@@ -0,0 +1,46 @@
+{
+ "description": "Repository intent survives readiness, config changes, and directory changes without deduplication or revision resolution",
+ "reducer": "session",
+ "initial": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ],
+ "config": {
+ "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } },
+ "values": { "mode": "review" }
+ }
+ },
+ "actions": [
+ { "type": "session/workingDirectorySet", "directory": "file:///work/project" },
+ { "type": "session/ready" },
+ { "type": "session/configChanged", "config": { "mode": "interactive" }, "replace": true },
+ { "type": "session/workingDirectorySet", "directory": "file:///work/project-worktree" },
+ { "type": "session/workingDirectoryRemoved", "directory": "file:///work/project" }
+ ],
+ "expected": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ],
+ "workingDirectories": ["file:///work/project-worktree"],
+ "config": {
+ "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } },
+ "values": { "mode": "interactive" }
+ }
+ }
+}
diff --git a/types/test-cases/reducers/273-repositories-survive-creation-failure.json b/types/test-cases/reducers/273-repositories-survive-creation-failure.json
new file mode 100644
index 00000000..654baee4
--- /dev/null
+++ b/types/test-cases/reducers/273-repositories-survive-creation-failure.json
@@ -0,0 +1,34 @@
+{
+ "description": "Failed preparation retains the exact repository list without claiming any checkout was prepared",
+ "reducer": "session",
+ "initial": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "main" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/other.git" }
+ ]
+ },
+ "actions": [
+ { "type": "session/creationFailed", "error": { "errorType": "preparationFailed", "message": "Preparation failed" } }
+ ],
+ "expected": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "main" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/other.git" }
+ ]
+ }
+}
diff --git a/types/test-cases/round-trips/045-session-config-without-repository.json b/types/test-cases/round-trips/045-session-config-without-repository.json
new file mode 100644
index 00000000..1b930eb0
--- /dev/null
+++ b/types/test-cases/round-trips/045-session-config-without-repository.json
@@ -0,0 +1,50 @@
+{
+ "name": "session-config-without-repository",
+ "group": "A",
+ "description": "An existing directory-backed session config remains valid without repository properties or values.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/directory-session",
+ "state": {
+ "provider": "example",
+ "title": "Directory session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": ["file:///work/existing"],
+ "config": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "mode": { "type": "string", "title": "Mode", "sessionMutable": true }
+ }
+ },
+ "values": { "mode": "review" }
+ }
+ },
+ "fromSeq": 1
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/directory-session",
+ "state": {
+ "provider": "example",
+ "title": "Directory session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": ["file:///work/existing"],
+ "config": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "mode": { "type": "string", "title": "Mode", "sessionMutable": true }
+ }
+ },
+ "values": { "mode": "review" }
+ }
+ },
+ "fromSeq": 1
+ }]
+}
diff --git a/types/test-cases/round-trips/046-repository-session-source-only.json b/types/test-cases/round-trips/046-repository-session-source-only.json
new file mode 100644
index 00000000..5cf29875
--- /dev/null
+++ b/types/test-cases/round-trips/046-repository-session-source-only.json
@@ -0,0 +1,34 @@
+{
+ "name": "repository-session-source-only",
+ "group": "A",
+ "description": "A ready session preserves typed source metadata, omits the optional revision, and resolves one source to multiple directories.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "workingDirectories": ["file:///work/project", "file:///work/project-worktree"]
+ },
+ "fromSeq": 2
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "workingDirectories": ["file:///work/project", "file:///work/project-worktree"]
+ },
+ "fromSeq": 2
+ }]
+}
diff --git a/types/test-cases/round-trips/047-repository-session-revision.json b/types/test-cases/round-trips/047-repository-session-revision.json
new file mode 100644
index 00000000..d2f26906
--- /dev/null
+++ b/types/test-cases/round-trips/047-repository-session-revision.json
@@ -0,0 +1,32 @@
+{
+ "name": "repository-session-revision",
+ "group": "A",
+ "description": "A creating session preserves typed source and revision metadata before a directory is resolved.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "refs/tags/v1.2.3" }]
+ },
+ "fromSeq": 0
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "refs/tags/v1.2.3" }]
+ },
+ "fromSeq": 0
+ }]
+}
diff --git a/types/test-cases/round-trips/048-repository-session-failed.json b/types/test-cases/round-trips/048-repository-session-failed.json
new file mode 100644
index 00000000..b2282eea
--- /dev/null
+++ b/types/test-cases/round-trips/048-repository-session-failed.json
@@ -0,0 +1,34 @@
+{
+ "name": "repository-session-failed",
+ "group": "A",
+ "description": "A failed session retains the requested repository source and revision without claiming a directory was resolved.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }]
+ },
+ "fromSeq": 1
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": [],
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }]
+ },
+ "fromSeq": 1
+ }]
+}
diff --git a/types/test-cases/round-trips/049-repository-preparation-capability.json b/types/test-cases/round-trips/049-repository-preparation-capability.json
new file mode 100644
index 00000000..23518205
--- /dev/null
+++ b/types/test-cases/round-trips/049-repository-preparation-capability.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-capability",
+ "group": "A",
+ "description": "An empty host capability supports one default-revision repository without an agent capability.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": {}
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": {}
+ }]
+}
diff --git a/types/test-cases/round-trips/050-repository-preparation-revisions.json b/types/test-cases/round-trips/050-repository-preparation-revisions.json
new file mode 100644
index 00000000..a17335aa
--- /dev/null
+++ b/types/test-cases/round-trips/050-repository-preparation-revisions.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-revisions",
+ "group": "A",
+ "description": "Host revision support does not imply multiple-repository support.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true }
+ }]
+}
diff --git a/types/test-cases/round-trips/051-repository-preparation-single.json b/types/test-cases/round-trips/051-repository-preparation-single.json
new file mode 100644
index 00000000..89b785db
--- /dev/null
+++ b/types/test-cases/round-trips/051-repository-preparation-single.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-single",
+ "group": "A",
+ "description": "Explicit false capability options round-trip without being confused with absence of host support.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": false, "multipleRepositories": false }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": false, "multipleRepositories": false }
+ }]
+}
diff --git a/types/test-cases/round-trips/052-repository-preparation-multiple.json b/types/test-cases/round-trips/052-repository-preparation-multiple.json
new file mode 100644
index 00000000..226d1faf
--- /dev/null
+++ b/types/test-cases/round-trips/052-repository-preparation-multiple.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-multiple",
+ "group": "A",
+ "description": "Future hosts can advertise multiple repositories and explicit revisions independently of agent capabilities.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true, "multipleRepositories": true }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true, "multipleRepositories": true }
+ }]
+}
diff --git a/types/test-cases/round-trips/053-repository-session-request-default-revision.json b/types/test-cases/round-trips/053-repository-session-request-default-revision.json
new file mode 100644
index 00000000..eef807c9
--- /dev/null
+++ b/types/test-cases/round-trips/053-repository-session-request-default-revision.json
@@ -0,0 +1,16 @@
+{
+ "name": "repository-session-request-default-revision",
+ "group": "A",
+ "description": "Creation uses a typed repository list and preserves omission of the requested revision.",
+ "type": "CreateSessionParams",
+ "input": {
+ "channel": "ahp-session:/repository-session",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/repository-session",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" }
+ }]
+}
diff --git a/types/test-cases/round-trips/054-repository-session-request-multiple.json b/types/test-cases/round-trips/054-repository-session-request-multiple.json
new file mode 100644
index 00000000..8503e6a2
--- /dev/null
+++ b/types/test-cases/round-trips/054-repository-session-request-multiple.json
@@ -0,0 +1,20 @@
+{
+ "name": "repository-session-request-multiple",
+ "group": "A",
+ "description": "A future multi-repository request preserves two revisions of the same source in their requested order.",
+ "type": "CreateSessionParams",
+ "input": {
+ "channel": "ahp-session:/repository-session",
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ]
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/repository-session",
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ]
+ }]
+}
diff --git a/types/test-cases/round-trips/055-repository-config-query-context.json b/types/test-cases/round-trips/055-repository-config-query-context.json
new file mode 100644
index 00000000..6874f6df
--- /dev/null
+++ b/types/test-cases/round-trips/055-repository-config-query-context.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-config-query-context",
+ "group": "A",
+ "description": "Repository lists and directory context coexist in configuration queries, outside generic config.",
+ "type": "ResolveSessionConfigParams",
+ "input": {
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }],
+ "config": { "mode": "review" }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }],
+ "config": { "mode": "review" }
+ }]
+}
diff --git a/types/test-cases/round-trips/056-repository-config-completions-context.json b/types/test-cases/round-trips/056-repository-config-completions-context.json
new file mode 100644
index 00000000..7c9dddd3
--- /dev/null
+++ b/types/test-cases/round-trips/056-repository-config-completions-context.json
@@ -0,0 +1,22 @@
+{
+ "name": "repository-config-completions-context",
+ "group": "A",
+ "description": "Configuration completions preserve the repository list and optional existing directory context.",
+ "type": "SessionConfigCompletionsParams",
+ "input": {
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" },
+ "property": "mode",
+ "query": "re"
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" },
+ "property": "mode",
+ "query": "re"
+ }]
+}
diff --git a/types/test-cases/round-trips/057-repository-session-summary.json b/types/test-cases/round-trips/057-repository-session-summary.json
new file mode 100644
index 00000000..9222a39d
--- /dev/null
+++ b/types/test-cases/round-trips/057-repository-session-summary.json
@@ -0,0 +1,34 @@
+{
+ "name": "repository-session-summary",
+ "group": "A",
+ "description": "Session summaries retain ordered repository intent, repeated sources, and omitted revisions.",
+ "type": "SessionSummary",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "createdAt": "2026-09-20T12:00:00.000Z",
+ "modifiedAt": "2026-09-20T12:00:00.000Z",
+ "repositories": [
+ { "source": "https://example.org/team/project.git" },
+ { "source": "https://example.org/team/project.git", "revision": "main" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" }
+ ],
+ "workingDirectories": ["file:///work/project", "file:///work/project-worktree"]
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "createdAt": "2026-09-20T12:00:00.000Z",
+ "modifiedAt": "2026-09-20T12:00:00.000Z",
+ "repositories": [
+ { "source": "https://example.org/team/project.git" },
+ { "source": "https://example.org/team/project.git", "revision": "main" },
+ { "source": "https://example.org/team/project.git", "revision": "feature" }
+ ],
+ "workingDirectories": ["file:///work/project", "file:///work/project-worktree"]
+ }]
+}
diff --git a/types/test-cases/round-trips/058-repository-session-multiple-ready.json b/types/test-cases/round-trips/058-repository-session-multiple-ready.json
new file mode 100644
index 00000000..ffcedc44
--- /dev/null
+++ b/types/test-cases/round-trips/058-repository-session-multiple-ready.json
@@ -0,0 +1,40 @@
+{
+ "name": "repository-session-multiple-ready",
+ "group": "A",
+ "description": "Ready state preserves repeated sources at different revisions without a positional directory mapping.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ],
+ "workingDirectories": ["file:///work/session-workspace"]
+ },
+ "fromSeq": 2
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ],
+ "workingDirectories": ["file:///work/session-workspace"]
+ },
+ "fromSeq": 2
+ }]
+}