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 + }] +}