Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d2537486 | ||
|
|
f70503d038 |
@@ -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);
|
||||
|
||||
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