Skip to content

Commit 42b7e0e

Browse files
Mikeclaude
andcommitted
feat(Workspace): Add session management and project tools
Session Management: - Enhanced ISessionManager with project loading/unloading - Added ProjectInfo for tracking loaded projects - Session now tracks exclude patterns and loaded projects - ActiveSessionProxy and ActiveWorkspaceProxy improvements New Workspace Tools: - ListProjectsTool: List all projects in solution with load status - LoadProjectTool: Load a specific project into the workspace - UnloadProjectTool: Unload a project from the workspace - SetExcludePatternsTool: Configure file/folder exclusion patterns Documentation: - Added AI workflow documentation (agents, gates, skills) - GitLab integration guide - Issue templates and workflow states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 970008c commit 42b7e0e

18 files changed

Lines changed: 1364 additions & 28 deletions

src/FractalDataWorks.Tools.Workspace/CreateSessionTool.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public sealed class CreateSessionTool : ToolBase
3232
public CreateSessionTool(
3333
ISessionManager sessionManager,
3434
ILogger<CreateSessionTool>? logger = null)
35-
: base("CreateSession", ToolCategories.Workspace, "Creates a new session for a solution file. Sessions provide isolated workspaces with tracking for conversation ID, description, and snapshots. Multiple sessions can work against the same solution with independent changes. Use ResumeSession to continue an existing session, or UpdateSession to set your conversation ID after creating a session.")
35+
: base("CreateSession", ToolCategories.Workspace, "Creates a new session for a solution file. By default, test projects are excluded to save resources (use excludePatterns='none' to load all). Sessions provide isolated workspaces with tracking for conversation ID, description, and snapshots. Returns: sessionId (string), projectCount (int), totalProjectCount (int), excludedProjectCount (int), excludePatterns (array). Use LoadProject to load excluded projects when needed.")
3636
{
3737
_sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager));
3838
_logger = logger ?? NullLogger<CreateSessionTool>.Instance;
@@ -55,6 +55,11 @@ public CreateSessionTool(
5555
"conversationId",
5656
"Optional conversation ID for Claude resume capability. Set this to track which conversation owns this session.",
5757
ToolParameterTypes.String,
58+
isRequired: false),
59+
new ToolParameter(
60+
"excludePatterns",
61+
"Glob patterns to exclude projects (e.g., '*.Tests,*.Benchmarks'), or 'default' for standard test project patterns, or 'none' to load all (default: 'default').",
62+
ToolParameterTypes.String,
5863
isRequired: false)
5964
];
6065

@@ -72,12 +77,31 @@ public override async Task<IGenericResult<ToolOutput>> Execute(
7277
return GenericResult<ToolOutput>.Failure(descriptionResult.CurrentMessage ?? "Description is required");
7378

7479
var conversationId = input.GetOptional<string>("conversationId");
80+
var excludePatternsInput = input.GetOptional("excludePatterns", "default");
81+
82+
// Parse exclude patterns
83+
IReadOnlyList<string> excludePatterns;
84+
if (string.Equals(excludePatternsInput, "default", StringComparison.OrdinalIgnoreCase))
85+
{
86+
excludePatterns = DefaultExcludePatterns.TestProjects;
87+
}
88+
else if (string.Equals(excludePatternsInput, "none", StringComparison.OrdinalIgnoreCase))
89+
{
90+
excludePatterns = DefaultExcludePatterns.None;
91+
}
92+
else
93+
{
94+
excludePatterns = excludePatternsInput!
95+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
96+
.ToList();
97+
}
7598

7699
try
77100
{
78101
var result = await _sessionManager.CreateSession(
79102
solutionPathResult.Value!,
80103
descriptionResult.Value!,
104+
excludePatterns,
81105
conversationId,
82106
setAsActive: true,
83107
cancellationToken).ConfigureAwait(false);
@@ -90,20 +114,30 @@ public override async Task<IGenericResult<ToolOutput>> Execute(
90114
var session = result.Value!;
91115
WorkspaceToolLog.SessionCreated(_logger, session.Id);
92116

117+
var loadedCount = session.TotalProjectCount - session.ExcludedProjectCount;
118+
var excludeNote = session.ExcludedProjectCount > 0
119+
? $" ({session.ExcludedProjectCount} projects excluded, use ListProjects to see them)"
120+
: "";
121+
93122
return GenericResult<ToolOutput>.Success(
94123
new ToolOutput(
95-
$"Created session '{session.Description}' for solution '{session.SolutionPath}'",
124+
$"Created session '{session.Description}' for solution '{session.SolutionPath}'{excludeNote}",
96125
new Dictionary<string, object>(StringComparer.Ordinal)
97126
{
98127
["sessionId"] = session.Id.ToString(),
99128
["solutionPath"] = session.SolutionPath,
100129
["description"] = session.Description,
101130
["conversationId"] = session.ConversationId ?? "",
102131
["projectCount"] = session.ProjectCount,
132+
["totalProjectCount"] = session.TotalProjectCount,
133+
["loadedProjectCount"] = loadedCount,
134+
["excludedProjectCount"] = session.ExcludedProjectCount,
135+
["excludePatterns"] = session.ExcludePatterns.ToList(),
103136
["createdAt"] = session.CreatedAt.ToString("O"),
104137
["isActive"] = true,
105-
["instructions"] = "Use UpdateSession to set your conversationId for resume capability. " +
106-
"Track this session ID in .claude/roslyn.sessions for future reference."
138+
["instructions"] = session.ExcludedProjectCount > 0
139+
? "Use ListProjects to see excluded projects. Use LoadProject to load a specific excluded project when needed."
140+
: "Use UpdateSession to set your conversationId for resume capability."
107141
}));
108142
}
109143
catch (Exception ex)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#pragma warning disable CA1305 // Specify IFormatProvider - workspace tools use invariant strings
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using FractalDataWorks.Collections.Attributes;
9+
using FractalDataWorks.Results;
10+
using FractalDataWorks.Tools.Abstractions;
11+
using FractalDataWorks.Tools.Abstractions.Categories;
12+
using FractalDataWorks.Tools.Abstractions.ParameterTypes;
13+
using FractalDataWorks.Tools.Workspace.Logging;
14+
using FractalDataWorks.Workspace.Roslyn;
15+
using Microsoft.Extensions.Logging;
16+
using Microsoft.Extensions.Logging.Abstractions;
17+
18+
namespace FractalDataWorks.Tools.Workspace;
19+
20+
/// <summary>
21+
/// Tool to list all projects in the solution with their load status.
22+
/// </summary>
23+
[TypeOption(typeof(ToolTypes), "ListProjects")]
24+
public sealed class ListProjectsTool : ToolBase
25+
{
26+
private readonly IRoslynWorkspace _workspace;
27+
private readonly ILogger<ListProjectsTool> _logger;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="ListProjectsTool"/> class.
31+
/// </summary>
32+
public ListProjectsTool(
33+
IRoslynWorkspace workspace,
34+
ILogger<ListProjectsTool>? logger = null)
35+
: base("ListProjects", ToolCategories.Workspace, "Lists all projects in the solution showing which are loaded, excluded, or unloaded. Use filter to show only specific projects. Returns: totalCount (int), loadedCount (int), excludedCount (int), excludePatterns (array), projects (array of {name, filePath, isLoaded, isExcluded, excludedByPattern, isTestProject, language, projectReferences, referencedBy}).")
36+
{
37+
_workspace = workspace ?? throw new ArgumentNullException(nameof(workspace));
38+
_logger = logger ?? NullLogger<ListProjectsTool>.Instance;
39+
}
40+
41+
/// <inheritdoc/>
42+
public override IReadOnlyList<ToolParameter> Parameters { get; } =
43+
[
44+
new ToolParameter("filter", "Filter: 'all' (default), 'loaded', 'excluded', 'test'", ToolParameterTypes.String, isRequired: false)
45+
];
46+
47+
/// <inheritdoc/>
48+
public override Task<IGenericResult<ToolOutput>> Execute(
49+
ToolInput input,
50+
CancellationToken cancellationToken = default)
51+
{
52+
var filter = input.GetOptional("filter", "all");
53+
54+
var allProjects = _workspace.GetAllProjects();
55+
var loadedProjects = _workspace.GetLoadedProjects();
56+
var excludedProjects = _workspace.GetExcludedProjects();
57+
var excludePatterns = _workspace.ExcludePatterns;
58+
59+
IReadOnlyList<ProjectInfo> filteredProjects = filter?.ToUpperInvariant() switch
60+
{
61+
"LOADED" => loadedProjects,
62+
"EXCLUDED" => excludedProjects,
63+
"TEST" => allProjects.Where(p => p.IsTestProject).ToList(),
64+
_ => allProjects
65+
};
66+
67+
WorkspaceToolLog.ProjectsListed(_logger, filteredProjects.Count, allProjects.Count);
68+
69+
var projectData = filteredProjects.Select(p => new Dictionary<string, object>(StringComparer.Ordinal)
70+
{
71+
["name"] = p.Name,
72+
["filePath"] = p.FilePath,
73+
["isLoaded"] = p.IsLoaded,
74+
["isExcluded"] = p.IsExcluded,
75+
["excludedByPattern"] = p.ExcludedByPattern ?? "",
76+
["isTestProject"] = p.IsTestProject,
77+
["language"] = p.Language ?? "Unknown",
78+
["projectReferences"] = p.ProjectReferences,
79+
["referencedBy"] = p.ReferencedBy
80+
}).ToList();
81+
82+
return Task.FromResult(GenericResult<ToolOutput>.Success(
83+
new ToolOutput(
84+
$"Found {filteredProjects.Count} projects ({loadedProjects.Count} loaded, {excludedProjects.Count} excluded)",
85+
new Dictionary<string, object>(StringComparer.Ordinal)
86+
{
87+
["totalCount"] = allProjects.Count,
88+
["loadedCount"] = loadedProjects.Count,
89+
["excludedCount"] = excludedProjects.Count,
90+
["excludePatterns"] = excludePatterns.ToList(),
91+
["filter"] = filter ?? "all",
92+
["projects"] = projectData
93+
})));
94+
}
95+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#pragma warning disable CA1305 // Specify IFormatProvider - workspace tools use invariant strings
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using FractalDataWorks.Collections.Attributes;
9+
using FractalDataWorks.Results;
10+
using FractalDataWorks.Tools.Abstractions;
11+
using FractalDataWorks.Tools.Abstractions.Categories;
12+
using FractalDataWorks.Tools.Abstractions.ParameterTypes;
13+
using FractalDataWorks.Tools.Workspace.Logging;
14+
using FractalDataWorks.Workspace.Roslyn;
15+
using Microsoft.Extensions.Logging;
16+
using Microsoft.Extensions.Logging.Abstractions;
17+
18+
namespace FractalDataWorks.Tools.Workspace;
19+
20+
/// <summary>
21+
/// Tool to load a project that was excluded from the workspace.
22+
/// </summary>
23+
[TypeOption(typeof(ToolTypes), "LoadProject")]
24+
public sealed class LoadProjectTool : ToolBase
25+
{
26+
private readonly IRoslynWorkspace _workspace;
27+
private readonly ILogger<LoadProjectTool> _logger;
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="LoadProjectTool"/> class.
31+
/// </summary>
32+
public LoadProjectTool(
33+
IRoslynWorkspace workspace,
34+
ILogger<LoadProjectTool>? logger = null)
35+
: base("LoadProject", ToolCategories.Workspace, "Loads an excluded project into the workspace. Use this when you need to work with a project that was excluded during session creation (e.g., test projects). Returns: name (string), filePath (string), isLoaded (bool), isTestProject (bool), projectReferences (array), referencedBy (array).")
36+
{
37+
_workspace = workspace ?? throw new ArgumentNullException(nameof(workspace));
38+
_logger = logger ?? NullLogger<LoadProjectTool>.Instance;
39+
}
40+
41+
/// <inheritdoc/>
42+
public override IReadOnlyList<ToolParameter> Parameters { get; } =
43+
[
44+
new ToolParameter("projectName", "Name of the project to load (e.g., 'MyProject.Tests')", ToolParameterTypes.String, isRequired: true)
45+
];
46+
47+
/// <inheritdoc/>
48+
public override async Task<IGenericResult<ToolOutput>> Execute(
49+
ToolInput input,
50+
CancellationToken cancellationToken = default)
51+
{
52+
var projectNameResult = input.GetRequired<string>("projectName");
53+
if (!projectNameResult.IsSuccess)
54+
return GenericResult<ToolOutput>.Failure(projectNameResult.CurrentMessage ?? "Project name is required");
55+
56+
var projectName = projectNameResult.Value!;
57+
58+
WorkspaceToolLog.LoadingProject(_logger, projectName);
59+
60+
var result = await _workspace.LoadProject(projectName, cancellationToken).ConfigureAwait(false);
61+
62+
if (!result.IsSuccess)
63+
{
64+
WorkspaceToolLog.ProjectLoadFailed(_logger, projectName, result.CurrentMessage ?? "Unknown error");
65+
return GenericResult<ToolOutput>.Failure(result.CurrentMessage ?? $"Failed to load project '{projectName}'");
66+
}
67+
68+
var project = result.Value!;
69+
70+
WorkspaceToolLog.ProjectLoaded(_logger, projectName);
71+
72+
return GenericResult<ToolOutput>.Success(
73+
new ToolOutput(
74+
$"Loaded project '{projectName}'",
75+
new Dictionary<string, object>(StringComparer.Ordinal)
76+
{
77+
["name"] = project.Name,
78+
["filePath"] = project.FilePath,
79+
["isLoaded"] = project.IsLoaded,
80+
["isTestProject"] = project.IsTestProject,
81+
["language"] = project.Language ?? "Unknown",
82+
["projectReferences"] = project.ProjectReferences,
83+
["referencedBy"] = project.ReferencedBy
84+
}));
85+
}
86+
}

src/FractalDataWorks.Tools.Workspace/Logging/WorkspaceToolLog.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,64 @@ public static partial class WorkspaceToolLog
242242
[MessageLogging(EventId = 7260, Level = LogLevel.Information,
243243
Message = "Retrieved test results: {testCount} tests")]
244244
public static partial IGenericMessage TestResultsRetrieved(ILogger logger, int testCount);
245+
246+
// ========================================================================
247+
// Project Load/Unload Tools (7350-7399)
248+
// ========================================================================
249+
250+
/// <summary>
251+
/// Logs when a project is being loaded.
252+
/// </summary>
253+
[MessageLogging(EventId = 7350, Level = LogLevel.Information,
254+
Message = "Loading project '{projectName}'")]
255+
public static partial IGenericMessage LoadingProject(ILogger logger, string projectName);
256+
257+
/// <summary>
258+
/// Logs when a project was loaded.
259+
/// </summary>
260+
[MessageLogging(EventId = 7351, Level = LogLevel.Information,
261+
Message = "Project loaded: '{projectName}'")]
262+
public static partial IGenericMessage ProjectLoaded(ILogger logger, string projectName);
263+
264+
/// <summary>
265+
/// Logs when loading a project fails.
266+
/// </summary>
267+
[MessageLogging(EventId = 7352, Level = LogLevel.Error,
268+
Message = "Failed to load project '{projectName}': {reason}")]
269+
public static partial IGenericMessage ProjectLoadFailed(ILogger logger, string projectName, string reason);
270+
271+
/// <summary>
272+
/// Logs when a project is being unloaded.
273+
/// </summary>
274+
[MessageLogging(EventId = 7360, Level = LogLevel.Information,
275+
Message = "Unloading project '{projectName}'")]
276+
public static partial IGenericMessage UnloadingProject(ILogger logger, string projectName);
277+
278+
/// <summary>
279+
/// Logs when a project was unloaded.
280+
/// </summary>
281+
[MessageLogging(EventId = 7361, Level = LogLevel.Information,
282+
Message = "Project unloaded: '{projectName}'")]
283+
public static partial IGenericMessage ProjectUnloaded(ILogger logger, string projectName);
284+
285+
/// <summary>
286+
/// Logs when unloading a project fails.
287+
/// </summary>
288+
[MessageLogging(EventId = 7362, Level = LogLevel.Error,
289+
Message = "Failed to unload project '{projectName}': {reason}")]
290+
public static partial IGenericMessage ProjectUnloadFailed(ILogger logger, string projectName, string reason);
291+
292+
/// <summary>
293+
/// Logs when projects are listed.
294+
/// </summary>
295+
[MessageLogging(EventId = 7370, Level = LogLevel.Information,
296+
Message = "Listed {filteredCount} of {totalCount} projects")]
297+
public static partial IGenericMessage ProjectsListed(ILogger logger, int filteredCount, int totalCount);
298+
299+
/// <summary>
300+
/// Logs when exclude patterns are updated.
301+
/// </summary>
302+
[MessageLogging(EventId = 7380, Level = LogLevel.Information,
303+
Message = "Exclude patterns updated: {patternCount} patterns")]
304+
public static partial IGenericMessage ExcludePatternsUpdated(ILogger logger, int patternCount);
245305
}

0 commit comments

Comments
 (0)