fix(worker): interaktive Session ohne Arbeitsverzeichnis im User-Verzeichnis starten
Eine manuelle Liste (Erinnerungen) hat kein WorkingDir. BuildForTaskAsync warf dafuer bisher "no working directory configured", die interaktive Session liess sich also nicht oeffnen. Jetzt faellt der Pfad auf das User-Verzeichnis zurueck; Session-Skills werden dort bewusst nicht geseedet, weil das Ziel das echte ~/.claude/skills waere.
This commit is contained in:
@@ -75,7 +75,8 @@ public sealed class InteractiveLaunchSpecService
|
||||
/// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException
|
||||
/// if it's Running/Queued. If the task has no usable worktree yet, one is created on
|
||||
/// demand (same mechanism as an autonomous run) provided the task's list has a working
|
||||
/// directory pointing at a git repo -- otherwise throws InvalidOperationException. Resumes
|
||||
/// directory pointing at a git repo; a non-repo directory is opened directly, and a list with
|
||||
/// no working directory at all (manual/reminder list) opens in the user's home dir. Resumes
|
||||
/// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if
|
||||
/// it has one, else the latest autonomous run's session; a task that has never run either
|
||||
/// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre-
|
||||
@@ -94,6 +95,7 @@ public sealed class InteractiveLaunchSpecService
|
||||
var isFreshWorktree = false;
|
||||
string sessionDir;
|
||||
bool sessionIsWorktree;
|
||||
var seedSkills = true;
|
||||
|
||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||
{
|
||||
@@ -105,10 +107,17 @@ public sealed class InteractiveLaunchSpecService
|
||||
// existing shape in the codebase (a list-handler task also has no worktree and commits
|
||||
// straight to the list's working dir), so no WorktreeEntity is created here either.
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct);
|
||||
if (list?.WorkingDir is null)
|
||||
throw new InvalidOperationException("This task has no working directory configured -- can't create a worktree.");
|
||||
|
||||
if (await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
||||
if (list is null || string.IsNullOrEmpty(list.WorkingDir))
|
||||
{
|
||||
// A manual/reminder list has no repo at all -- open the session in the user's
|
||||
// home directory instead of refusing it. Skills are deliberately NOT seeded
|
||||
// there: the destination would be the user's REAL ~/.claude/skills, not a
|
||||
// throwaway session copy.
|
||||
sessionDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
sessionIsWorktree = false;
|
||||
seedSkills = false;
|
||||
}
|
||||
else if (await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
||||
{
|
||||
// A stale (Merged/Discarded) row is already tracked in `ctx`'s identity map from
|
||||
// the read above -- CreateAsync replaces it via its OWN context, so re-querying on
|
||||
@@ -146,9 +155,12 @@ public sealed class InteractiveLaunchSpecService
|
||||
// history the task accumulated before its previous worktree went away.
|
||||
var run = isFreshWorktree ? null : await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct);
|
||||
|
||||
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
||||
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
||||
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
||||
if (seedSkills)
|
||||
{
|
||||
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
||||
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
||||
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
||||
}
|
||||
|
||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||
|
||||
@@ -220,8 +220,15 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForTaskAsync_NoWorktreeRow_NoWorkingDirConfigured_ThrowsInvalidOperation()
|
||||
public async Task BuildForTaskAsync_NoWorktreeRow_NoWorkingDirConfigured_OpensInUserHomeWithoutSeedingSkills()
|
||||
{
|
||||
_registry.Installed.Add(new SessionSkillEntity
|
||||
{
|
||||
Name = "installed-skill", SourceUrl = "https://example.com/x.git",
|
||||
PinnedRef = "abc", Subpath = "skills/installed-skill", Description = "d",
|
||||
AddedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
|
||||
var listId = await SeedListAsync();
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
@@ -230,11 +237,18 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.Idle,
|
||||
sessionSkillsJson: "[\"installed-skill\"]");
|
||||
|
||||
var svc = BuildService();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => svc.BuildForTaskAsync(taskId, CancellationToken.None));
|
||||
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), spec.Cwd);
|
||||
// Seeding here would copy into the user's REAL ~/.claude/skills.
|
||||
Assert.Empty(_seeder.Calls);
|
||||
|
||||
using var readCtx = _db.CreateContext();
|
||||
Assert.Null(await new WorktreeRepository(readCtx).GetByTaskIdAsync(taskId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user