@kendoo.agentdesk/agentdesk 0.29.1 → 0.29.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,11 @@ All user-facing changes to AgentDesk. Each entry is tagged:
8
8
 
9
9
  Internal refactors, infrastructure changes, and architectural notes are not listed here.
10
10
 
11
+ ## [0.29.3] — 2026-09-21
12
+
13
+ ### Fixed
14
+ - `[CLI]` The dashboard's new task-lookup (which recommends resuming, continuing, or starting fresh for a task id) could miss a workspace that already had unfinished local work, then try to check out its branch a second time and fail with `fatal: '...' is already used by worktree at ...`. Lookups now match by the repo's actual path instead of a locally-derived project label that could go stale. (Published as 0.29.3 — 0.29.2 got stuck mid-publish on the registry.)
15
+
11
16
  ## [0.29.1] — 2026-09-20
12
17
 
13
18
  ### Fixed
package/cli/daemon.mjs CHANGED
@@ -15,7 +15,7 @@ import { getRegisteredProjects, registerLocalProject, claimLocalProjects } from
15
15
  import { buildTrackerUrl } from "./tracker-url.mjs";
16
16
  import { fileURLToPath } from "url";
17
17
  import { dirname } from "path";
18
- import { repositoryInfo, listWorkspaces, inspectWorkspace, cleanupWorkspace, recoverWorkspaceLocks } from "./worktrees.mjs";
18
+ import { repositoryInfo, listWorkspaces, findWorkspacesByTask, inspectWorkspace, cleanupWorkspace, recoverWorkspaceLocks } from "./worktrees.mjs";
19
19
  import { createSessionQueue, sessionLimit, confirmWithAbort } from "./session-queue.mjs";
20
20
 
21
21
  const __dirname_daemon = dirname(fileURLToPath(import.meta.url));
@@ -419,7 +419,7 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
419
419
 
420
420
  // 4. Session handling
421
421
 
422
- function handleWorkspaceRequest({ requestId, action, projectId, workspaceId, discard, confirmation }) {
422
+ function handleWorkspaceRequest({ requestId, action, projectId, workspaceId, discard, confirmation, taskId }) {
423
423
  try {
424
424
  const projectPaths = projects.map(p => p.path);
425
425
  let result;
@@ -435,6 +435,15 @@ async function confirmIncomingSession({ project, taskId, prompt, signal }) {
435
435
  }
436
436
  return inspectWorkspace(record);
437
437
  });
438
+ } else if (action === "lookup") {
439
+ const project = projects.find(p => p.id === projectId);
440
+ if (!project) throw new Error("Unknown project");
441
+ result = findWorkspacesByTask({ projectPath: project.path, taskId }).map(record => {
442
+ if (record.archived && !record.removedAt) {
443
+ try { return cleanupWorkspace(record.id, { projectPaths }); } catch {}
444
+ }
445
+ return inspectWorkspace(record);
446
+ });
438
447
  } else if (action === "cleanup" || action === "archive") {
439
448
  result = cleanupWorkspace(workspaceId, { projectPaths, discard: discard === true, confirmation, archive: action === "archive" });
440
449
  } else throw new Error("Unknown workspace action");
package/cli/worktrees.mjs CHANGED
@@ -77,6 +77,18 @@ export function listWorkspaces({ root, projectPaths } = {}) {
77
77
  });
78
78
  }
79
79
 
80
+ // Matches on the resolved repo path, not the workspace record's stored
81
+ // projectId — that field is client-derived (falls back to package.json's
82
+ // name when a project isn't fully configured) and can silently drift from
83
+ // the server's project id, which would make an unfinished workspace
84
+ // invisible to lookups and risk a second `git worktree add` colliding with
85
+ // the branch it already holds.
86
+ export function findWorkspacesByTask({ root, projectPath, taskId } = {}) {
87
+ if (!taskId || !projectPath) return [];
88
+ const resolved = realpathSync(projectPath);
89
+ return listWorkspaces({ root }).filter(record => record.sourceCwd === resolved && record.taskId === taskId);
90
+ }
91
+
80
92
  export function getWorkspace(id, { root, projectPaths } = {}) {
81
93
  const record = read(join(rootPath(root), `${hash(id)}.json`));
82
94
  if (record.id !== id || (projectPaths && !projectPaths.some(p => realpathSync(p) === record.sourceCwd))) throw new Error("Workspace not found for this project.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kendoo.agentdesk/agentdesk",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "AI team orchestrator for Claude Code — run collaborative agent sessions from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "server": "node server/index.mjs",
23
23
  "build": "vite build",
24
24
  "preview": "vite preview",
25
- "test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs tests/projects-registry.test.mjs tests/phase-loop.test.mjs tests/proc.test.mjs tests/crypto.test.mjs tests/dotenv.test.mjs tests/update-check.test.mjs tests/setup-helpers.test.mjs tests/project-key.test.mjs tests/tracker-project.test.mjs tests/tracker-check.test.mjs tests/config.test.mjs tests/engine-env.test.mjs tests/engine-events.test.mjs tests/engine-verdict.test.mjs tests/engine-hooks.test.mjs tests/engine-agents.test.mjs tests/session-isolation.test.mjs tests/engine-session.test.mjs tests/engine-prompts.test.mjs tests/engine-schemas.test.mjs tests/engine-claude-auth.test.mjs tests/worktrees.test.mjs tests/workspaces-api.test.mjs tests/engine-outcome.test.mjs tests/session-outcomes.test.mjs tests/outcome-hydration.test.mjs tests/mobile-outcomes.test.mjs tests/session-queue.test.mjs tests/useFollowScroll.test.mjs tests/session-usage.test.mjs",
25
+ "test": "node --test tests/server.test.mjs tests/agents.test.mjs tests/homepage.test.mjs tests/sessionUtils.test.mjs tests/tracker-url.test.mjs tests/session-preflight.test.mjs tests/access.test.mjs tests/project-ownership.test.mjs tests/random-hex.test.mjs tests/projects-registry.test.mjs tests/phase-loop.test.mjs tests/proc.test.mjs tests/crypto.test.mjs tests/dotenv.test.mjs tests/update-check.test.mjs tests/setup-helpers.test.mjs tests/project-key.test.mjs tests/tracker-project.test.mjs tests/tracker-check.test.mjs tests/config.test.mjs tests/engine-env.test.mjs tests/engine-events.test.mjs tests/engine-verdict.test.mjs tests/engine-hooks.test.mjs tests/engine-agents.test.mjs tests/session-isolation.test.mjs tests/engine-session.test.mjs tests/engine-prompts.test.mjs tests/engine-schemas.test.mjs tests/engine-claude-auth.test.mjs tests/worktrees.test.mjs tests/workspaces-api.test.mjs tests/engine-outcome.test.mjs tests/session-outcomes.test.mjs tests/outcome-hydration.test.mjs tests/mobile-outcomes.test.mjs tests/session-queue.test.mjs tests/useFollowScroll.test.mjs tests/session-usage.test.mjs tests/task-lookup.test.mjs",
26
26
  "test:coverage": "node --test --experimental-test-coverage --test-coverage-include='cli/**' --test-coverage-include='server/**' --test-coverage-lines=60 --test-coverage-branches=62 tests/*.test.mjs",
27
27
  "lint": "eslint .",
28
28
  "lint:fix": "eslint . --fix",
@@ -0,0 +1,41 @@
1
+ // Pure decision logic for "what should agentdesk do with this task id" —
2
+ // kept here so the server route and its tests share one source of truth.
3
+ const PR_OUTCOME_KIND = "pr_created";
4
+
5
+ function toCandidate(workspace) {
6
+ return { workspaceId: workspace.id, branch: workspace.branch, reason: workspace.reason };
7
+ }
8
+
9
+ export function recommendTaskAction({ workspaces = [], sessions = [] } = {}) {
10
+ const liveWorkspaces = workspaces.filter(w => !w.removedAt);
11
+
12
+ const inUse = liveWorkspaces.filter(w => w.active);
13
+ if (inUse.length === 1) return { action: "in_use", workspace: inUse[0], reason: inUse[0].reason };
14
+ if (inUse.length > 1) return { action: "multiple", candidates: inUse.map(toCandidate) };
15
+
16
+ const unfinished = liveWorkspaces.filter(w => !w.safe);
17
+ if (unfinished.length === 1) return { action: "resume", workspace: unfinished[0], reason: unfinished[0].reason };
18
+ if (unfinished.length > 1) return { action: "multiple", candidates: unfinished.map(toCandidate) };
19
+
20
+ const merged = liveWorkspaces.filter(w => w.safe);
21
+
22
+ const prOutcomes = sessions.flatMap(s => (s.outcomes || [])
23
+ .filter(o => o.kind === PR_OUTCOME_KIND)
24
+ .map(o => ({ ...o, sessionId: s.sessionId })));
25
+ if (prOutcomes.length > 0) {
26
+ const latest = prOutcomes.reduce((a, b) => (a.observedAt >= b.observedAt ? a : b));
27
+ return { action: "continue_pr", url: latest.url, branch: latest.branch || null, sessionId: latest.sessionId };
28
+ }
29
+
30
+ if (merged.length > 0) return { action: "already_merged", workspace: merged[0] };
31
+
32
+ const branches = [...new Set(sessions.map(s => s.branch || s.workspace?.branch).filter(Boolean))];
33
+ if (branches.length === 1) {
34
+ const branch = branches[0];
35
+ const session = sessions.find(s => (s.branch || s.workspace?.branch) === branch);
36
+ return { action: "continue_branch", branch, sessionId: session?.sessionId || null };
37
+ }
38
+ if (branches.length > 1) return { action: "multiple", candidates: branches.map(branch => ({ branch })) };
39
+
40
+ return { action: "start_new" };
41
+ }