Compare commits
2
Commits
9360cd2fe6
...
40d2537486
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d2537486 | ||
|
|
f70503d038 |
@@ -75,7 +75,8 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
/// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException
|
/// 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
|
/// 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
|
/// 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
|
/// (--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
|
/// 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-
|
/// 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;
|
var isFreshWorktree = false;
|
||||||
string sessionDir;
|
string sessionDir;
|
||||||
bool sessionIsWorktree;
|
bool sessionIsWorktree;
|
||||||
|
var seedSkills = true;
|
||||||
|
|
||||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
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
|
// 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.
|
// straight to the list's working dir), so no WorktreeEntity is created here either.
|
||||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct);
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct);
|
||||||
if (list?.WorkingDir is null)
|
if (list is null || string.IsNullOrEmpty(list.WorkingDir))
|
||||||
throw new InvalidOperationException("This task has no working directory configured -- can't create a worktree.");
|
{
|
||||||
|
// A manual/reminder list has no repo at all -- open the session in the user's
|
||||||
if (await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
// 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
|
// 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
|
// 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.
|
// history the task accumulated before its previous worktree went away.
|
||||||
var run = isFreshWorktree ? null : await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct);
|
var run = isFreshWorktree ? null : await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct);
|
||||||
|
|
||||||
|
if (seedSkills)
|
||||||
|
{
|
||||||
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
||||||
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
||||||
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
||||||
|
}
|
||||||
|
|
||||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||||
|
|||||||
@@ -220,8 +220,15 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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();
|
var listId = await SeedListAsync();
|
||||||
using (var ctx = _db.CreateContext())
|
using (var ctx = _db.CreateContext())
|
||||||
{
|
{
|
||||||
@@ -230,11 +237,18 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
var taskId = Guid.NewGuid().ToString();
|
var taskId = Guid.NewGuid().ToString();
|
||||||
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
await SeedTaskAsync(taskId, listId, TaskStatus.Idle,
|
||||||
|
sessionSkillsJson: "[\"installed-skill\"]");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
() => 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]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user