@minimalcorp/tsunagi 0.1.44 → 0.1.46

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.
Files changed (96) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.rsc +2 -2
  32. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +2 -2
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  38. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  39. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0ze019p._.js → [root-of-the-server]__00qvk7s._.js} +2 -2
  41. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d7excn._.js +2 -2
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__13yhig0._.js → [root-of-the-server]__0n83hx6._.js} +2 -2
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w3_4.k._.js +2 -2
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +13 -13
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0lgu9gx._.js → node_modules_049fpp9._.js} +1 -1
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0f3mifg._.js → node_modules_04g5ove._.js} +1 -1
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0z02r3w._.js → node_modules_076bd-f._.js} +1 -1
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_07f58ia._.js → node_modules_0akt-n8._.js} +1 -1
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0p2g6on._.js → node_modules_0b48qku._.js} +1 -1
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0szd5t1._.js → node_modules_0bnsxo_._.js} +1 -1
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ih0wxv._.js → node_modules_0bvpcr6._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0bq-u.l._.js → node_modules_0dmu3re._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0~pcd4h._.js → node_modules_0gq_a6h._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nwi8c2._.js → node_modules_0p8a~x3._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06uw9~g._.js → node_modules_0pksdi6._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_00z7dqq._.js → node_modules_0qvxljj._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0vtmkdp._.js → node_modules_0soq_if._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_07ve26r._.js → node_modules_0v6uk6x._.js} +1 -1
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_00~3~7_._.js → node_modules_0ydrd8p._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10mbxmu._.js → node_modules_0zir34w._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  63. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  64. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  65. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  66. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  67. package/.next/standalone/apps/web/.next/static/chunks/{0wvv75.1gvloi.js → 0.ud795.723zr.js} +13 -13
  68. package/.next/standalone/apps/web/.next/static/chunks/{0.e10jueei8qv.js → 04fnr6blquen6.js} +3 -3
  69. package/.next/standalone/apps/web/.next/static/chunks/04h-q2fe2awuu.css +4 -0
  70. package/.next/standalone/apps/web/.next/static/chunks/{0u_07wgcigiqu.js → 0pfyw6txeoy_p.js} +7 -7
  71. package/.next/standalone/apps/web/.next/static/chunks/{10usg-2-r6y3f.js → 0z43cogzp17lz.js} +3 -3
  72. package/dist/prisma/migrations/20260929000000_drop_task_worktree_status/migration.sql +31 -0
  73. package/dist/prisma/schema.prisma +0 -1
  74. package/dist/server/generated/prisma/internal/class.js +4 -4
  75. package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +0 -1
  76. package/dist/server/generated/prisma/internal/prismaNamespace.js +0 -1
  77. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +0 -1
  78. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +0 -1
  79. package/dist/server/generated/prisma/models/Task.d.ts +1 -40
  80. package/dist/server/lib/branch-utils.d.ts +1 -0
  81. package/dist/server/lib/branch-utils.js +30 -0
  82. package/dist/server/lib/repositories/task.js +0 -3
  83. package/dist/server/lib/services/task-service.d.ts +10 -2
  84. package/dist/server/lib/services/task-service.js +79 -48
  85. package/dist/server/lib/worktree-manager.d.ts +2 -0
  86. package/dist/server/lib/worktree-manager.js +22 -0
  87. package/dist/server/routes/mcp.js +7 -2
  88. package/dist/server/routes/repos.js +20 -15
  89. package/dist/server/routes/tasks.js +22 -23
  90. package/dist/server/routes/terminal.js +6 -1
  91. package/npm-shrinkwrap.json +5 -5
  92. package/package.json +1 -1
  93. package/.next/standalone/apps/web/.next/static/chunks/09osn8dd42~wz.css +0 -4
  94. /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_buildManifest.js +0 -0
  95. /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_clientMiddlewareManifest.js +0 -0
  96. /package/.next/standalone/apps/web/.next/static/{b89JVAeQs5UYaMHr5WMOT → eJVDCrc-0k3bUASWyCnDU}/_ssgManifest.js +0 -0
@@ -22,8 +22,8 @@ export interface CreateTaskParams {
22
22
  status?: Task['status'];
23
23
  }
24
24
  export declare class TaskServiceError extends Error {
25
- code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR';
26
- constructor(message: string, code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR');
25
+ code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'INVALID_BRANCH' | 'WORKTREE_CREATION_FAILED' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR';
26
+ constructor(message: string, code: 'REPO_NOT_FOUND' | 'BRANCH_DUPLICATE' | 'INVALID_BRANCH' | 'WORKTREE_CREATION_FAILED' | 'TASK_NOT_FOUND' | 'IDENTIFIER_REQUIRED' | 'INTERNAL_ERROR');
27
27
  }
28
28
  /**
29
29
  * id / session_id / cwd からタスクを解決する
@@ -42,11 +42,19 @@ export declare function listTasks(filter?: {
42
42
  * タスクを取得(identifier で解決)
43
43
  */
44
44
  export declare function getTask(identifier: TaskIdentifier): Promise<Task>;
45
+ /**
46
+ * 新規タスクのブランチ名を検証する(worktree 作成に失敗する値を事前に弾く)
47
+ */
48
+ export declare function validateNewTaskBranch(owner: string, repo: string, branch: string): Promise<void>;
45
49
  /**
46
50
  * タスクを作成
51
+ *
52
+ * worktree 作成に成功した場合のみタスクを登録する(worktree の無いタスクを作らない)。
53
+ * fetch 失敗時はローカルの参照で worktree を作成し、warnings で通知する。
47
54
  */
48
55
  export declare function createTask(params: CreateTaskParams, options?: ServiceOptions): Promise<{
49
56
  task: Task;
57
+ warnings: string[];
50
58
  }>;
51
59
  /**
52
60
  * タスクを更新(identifier で解決)
@@ -5,7 +5,7 @@ import * as os from 'os';
5
5
  import * as taskRepo from '../repositories/task.js';
6
6
  import * as repoRepo from '../repositories/repository.js';
7
7
  import * as worktreeManager from '../worktree-manager.js';
8
- import { normalizeBranchName } from '../branch-utils.js';
8
+ import { normalizeBranchName, validateBranchName } from '../branch-utils.js';
9
9
  import { prisma } from '../db.js';
10
10
  export class TaskServiceError extends Error {
11
11
  code;
@@ -90,73 +90,104 @@ export async function getTask(identifier) {
90
90
  }
91
91
  return task;
92
92
  }
93
+ /**
94
+ * 新規タスクのブランチ名を検証する(worktree 作成に失敗する値を事前に弾く)
95
+ */
96
+ export async function validateNewTaskBranch(owner, repo, branch) {
97
+ const invalidReason = validateBranchName(branch);
98
+ if (invalidReason || !(await worktreeManager.isValidBranchRef(branch))) {
99
+ throw new TaskServiceError(`Invalid branch name "${branch}": ${invalidReason ?? 'rejected by git check-ref-format'}`, 'INVALID_BRANCH');
100
+ }
101
+ const existingTasks = await taskRepo.getTasks({ owner, repo, includeDeleted: false });
102
+ const duplicateTask = existingTasks.find((task) => task.branch === branch);
103
+ if (duplicateTask) {
104
+ throw new TaskServiceError(`Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`, 'BRANCH_DUPLICATE');
105
+ }
106
+ // "/" は "-" に正規化されるため、別名のブランチでも worktree ディレクトリが衝突しうる
107
+ if (await worktreeManager.worktreeExists(owner, repo, branch)) {
108
+ throw new TaskServiceError(`Worktree directory "${normalizeBranchName(branch)}" already exists. Use a different branch name.`, 'BRANCH_DUPLICATE');
109
+ }
110
+ }
93
111
  /**
94
112
  * タスクを作成
113
+ *
114
+ * worktree 作成に成功した場合のみタスクを登録する(worktree の無いタスクを作らない)。
115
+ * fetch 失敗時はローカルの参照で worktree を作成し、warnings で通知する。
95
116
  */
96
117
  export async function createTask(params, options) {
97
118
  const { title, description = '', owner, repo, baseBranch: baseBranchInput, effort, order, status = 'backlog', } = params;
119
+ const warnings = [];
98
120
  // リポジトリ存在チェック
99
121
  const repository = await repoRepo.getRepo(owner, repo);
100
122
  if (!repository) {
101
123
  throw new TaskServiceError('Repository not found. Please clone the repository first.', 'REPO_NOT_FOUND');
102
124
  }
103
- // baseBranch解決
104
- const baseBranch = baseBranchInput || (await worktreeManager.getDefaultBranch(owner, repo));
105
125
  // branch名の解決(未指定の場合はtitleから自動生成)
106
126
  const branch = params.branch || generateBranchName(title);
107
- // ブランチ名重複チェック
108
- const existingTasks = await taskRepo.getTasks({ includeDeleted: false });
109
- const duplicateTask = existingTasks.find((task) => task.owner === owner && task.repo === repo && task.branch === branch);
110
- if (duplicateTask) {
111
- throw new TaskServiceError(`Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`, 'BRANCH_DUPLICATE');
112
- }
113
- // 1. order解決 + bumpOrder + DB登録をアトミックに実行
114
- const newTask = await prisma.$transaction(async (tx) => {
115
- let resolvedOrder;
116
- if (order !== undefined) {
117
- resolvedOrder = order;
118
- await taskRepo.bumpOrder(resolvedOrder, undefined, tx);
119
- }
120
- else {
121
- const result = await tx.task.aggregate({
122
- where: { owner, repo, deletedAt: null },
123
- _max: { order: true },
124
- });
125
- resolvedOrder = (result._max.order ?? -1) + 1;
126
- }
127
- return await taskRepo.createTask({
128
- title,
129
- description,
130
- status,
131
- owner,
132
- repo,
133
- branch,
134
- baseBranch,
135
- repoId: repository.id,
136
- worktreeStatus: 'pending',
137
- effort,
138
- order: resolvedOrder,
139
- }, tx);
140
- });
141
- // 3. worktree作成
127
+ await validateNewTaskBranch(owner, repo, branch);
128
+ // 1. fetch(失敗してもローカルの参照で続行する)
142
129
  try {
143
130
  await worktreeManager.fetchRemote(owner, repo);
131
+ }
132
+ catch (error) {
133
+ console.warn('Failed to fetch remote:', error);
134
+ warnings.push('Failed to fetch from remote. The worktree was created from local refs; update it once the network is available.');
135
+ }
136
+ // baseBranch解決
137
+ const baseBranch = baseBranchInput || (await worktreeManager.getDefaultBranch(owner, repo));
138
+ // 2. worktree作成(失敗したらタスクは作らない)
139
+ try {
144
140
  await worktreeManager.createWorktree(owner, repo, branch, baseBranch);
145
- await taskRepo.updateTask(newTask.id, { worktreeStatus: 'created' });
146
141
  }
147
142
  catch (error) {
148
143
  console.error('Failed to create worktree:', error);
149
- await taskRepo.updateTask(newTask.id, { worktreeStatus: 'error' });
144
+ const reason = error instanceof Error ? error.message : String(error);
145
+ throw new TaskServiceError(`Failed to create worktree: ${reason}`, 'WORKTREE_CREATION_FAILED');
150
146
  }
151
- // 初期Tabは作らない。タブはユーザーがタスク詳細で起動先(Claude / Ollama 等)を選んで作る
152
- // 4. 更新後のタスクを取得
153
- const updatedTask = await taskRepo.getTask(newTask.id);
154
- if (!updatedTask) {
155
- throw new TaskServiceError('Failed to retrieve created task', 'INTERNAL_ERROR');
147
+ // 3. order解決 + bumpOrder + DB登録をアトミックに実行(失敗したら worktree を削除)
148
+ let newTask;
149
+ try {
150
+ newTask = await prisma.$transaction(async (tx) => {
151
+ let resolvedOrder;
152
+ if (order !== undefined) {
153
+ resolvedOrder = order;
154
+ await taskRepo.bumpOrder(resolvedOrder, undefined, tx);
155
+ }
156
+ else {
157
+ const result = await tx.task.aggregate({
158
+ where: { owner, repo, deletedAt: null },
159
+ _max: { order: true },
160
+ });
161
+ resolvedOrder = (result._max.order ?? -1) + 1;
162
+ }
163
+ return await taskRepo.createTask({
164
+ title,
165
+ description,
166
+ status,
167
+ owner,
168
+ repo,
169
+ branch,
170
+ baseBranch,
171
+ repoId: repository.id,
172
+ effort,
173
+ order: resolvedOrder,
174
+ }, tx);
175
+ });
176
+ }
177
+ catch (error) {
178
+ // force=false: 既存のローカルブランチを流用した場合に削除しないよう、ブランチは残す
179
+ try {
180
+ await worktreeManager.removeWorktree(owner, repo, branch);
181
+ }
182
+ catch (cleanupError) {
183
+ console.error('Failed to remove worktree after task creation failure:', cleanupError);
184
+ }
185
+ throw error;
156
186
  }
157
- // 5. Socket.IO通知
158
- options?.io?.emit('task:created', { task: updatedTask });
159
- return { task: updatedTask };
187
+ // 初期Tabは作らない。タブはユーザーがタスク詳細で起動先(Claude / Ollama 等)を選んで作る
188
+ // 4. Socket.IO通知
189
+ options?.io?.emit('task:created', { task: newTask, warnings });
190
+ return { task: newTask, warnings };
160
191
  }
161
192
  /**
162
193
  * タスクを更新(identifier で解決)
@@ -12,6 +12,8 @@ export declare function getRemoteBranches(owner: string, repo: string): Promise<
12
12
  export declare function isEmptyRepo(owner: string, repo: string): Promise<boolean>;
13
13
  export declare function isUnbornWorktree(worktreePath: string): Promise<boolean>;
14
14
  export declare function getDefaultBranch(owner: string, repo: string): Promise<string>;
15
+ export declare function isValidBranchRef(branch: string): Promise<boolean>;
16
+ export declare function worktreeExists(owner: string, repo: string, branch: string): Promise<boolean>;
15
17
  export declare function createWorktree(owner: string, repo: string, branch: string, baseBranch?: string): Promise<{
16
18
  worktreePath: string;
17
19
  }>;
@@ -169,6 +169,28 @@ export async function getDefaultBranch(owner, repo) {
169
169
  return branches[0] || 'main';
170
170
  }
171
171
  }
172
+ // git 自身のルールでブランチ名として有効か判定(最終判定用)
173
+ // --branch はカレントのリポジトリ探索を伴い cwd 次第で失敗するため、refs/heads/ 形式で判定する
174
+ // ("-" 始まり・"@"・"HEAD" は validateBranchName 側で弾く)
175
+ export async function isValidBranchRef(branch) {
176
+ try {
177
+ await simpleGit().raw(['check-ref-format', `refs/heads/${branch}`]);
178
+ return true;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ // worktreeディレクトリが存在するか確認
185
+ export async function worktreeExists(owner, repo, branch) {
186
+ try {
187
+ await fs.access(getWorktreePath(owner, repo, branch));
188
+ return true;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
172
194
  // worktreeを作成
173
195
  export async function createWorktree(owner, repo, branch, baseBranch) {
174
196
  const bareRepoPath = await ensureBareRepository(owner, repo);
@@ -93,7 +93,7 @@ function createMcpServer(io) {
93
93
  },
94
94
  {
95
95
  name: 'tsunagi_create_task',
96
- description: 'タスクを作成する。worktree作成・初期タブ作成・Socket.IO通知を含むフル作成フロー。branchを省略するとtitleから自動生成される。',
96
+ description: 'タスクを作成する。worktree作成・初期タブ作成・Socket.IO通知を含むフル作成フロー。branchを省略するとtitleから自動生成される。worktree作成に失敗した場合(gitで不正なブランチ名等)はタスクは作成されない。',
97
97
  inputSchema: {
98
98
  type: 'object',
99
99
  required: ['owner', 'repo', 'title'],
@@ -231,7 +231,12 @@ function createMcpServer(io) {
231
231
  branch,
232
232
  baseBranch,
233
233
  }, { io });
234
- return { content: [{ type: 'text', text: JSON.stringify(result.task, null, 2) }] };
234
+ return {
235
+ content: [
236
+ { type: 'text', text: JSON.stringify(result.task, null, 2) },
237
+ ...result.warnings.map((warning) => ({ type: 'text', text: `Warning: ${warning}` })),
238
+ ],
239
+ };
235
240
  }
236
241
  case 'tsunagi_update_task': {
237
242
  const { id, session_id, cwd, title, description, status, effort, order, baseBranch, pullRequestUrl, } = (args ?? {});
@@ -215,39 +215,44 @@ export async function reposRoutes(fastify) {
215
215
  fastify.log.warn(cleanupError, 'Failed to cleanup bare repository after clone failure');
216
216
  }
217
217
  };
218
- const sshUrl = `git@github.com:${owner}/${repo}.git`;
219
218
  const isHttpsUrl = normalizedUrl.startsWith('http');
219
+ const protocol = isHttpsUrl ? 'HTTPS' : 'SSH';
220
+ const altProtocol = isHttpsUrl ? 'SSH' : 'HTTPS';
221
+ // 認証エラー時にもう一方の方式で試し直すためのURL
222
+ const altUrl = isHttpsUrl
223
+ ? `git@github.com:${owner}/${repo}.git`
224
+ : `https://github.com/${owner}/${repo}.git`;
220
225
  let clonedUrl = normalizedUrl;
221
226
  try {
222
227
  await worktreeManager.initBareRepository(owner, repo, normalizedUrl);
223
228
  }
224
229
  catch (error) {
225
230
  await cleanupBareRepo();
226
- // private repository を HTTPS で clone するには認証が必要。
227
- // tsunagi は SSH agent 前提で動くため、認証エラーなら SSH で自動的に試し直す。
228
- if (!isHttpsUrl || !isGitAuthError(error))
231
+ // 認証手段は環境により異なる(SSH鍵のみ / HTTPS + credential helper のみ 等)。
232
+ // 認証エラーならもう一方の方式で自動的に試し直す。
233
+ if (!isGitAuthError(error))
229
234
  throw error;
230
- fastify.log.info({ owner, repo }, 'HTTPS clone failed with an auth error, retrying over SSH');
235
+ fastify.log.info({ owner, repo }, `${protocol} clone failed with an auth error, retrying over ${altProtocol}`);
231
236
  try {
232
- await worktreeManager.initBareRepository(owner, repo, sshUrl);
233
- clonedUrl = sshUrl;
237
+ await worktreeManager.initBareRepository(owner, repo, altUrl);
238
+ clonedUrl = altUrl;
234
239
  }
235
- catch (sshError) {
240
+ catch (altError) {
236
241
  await cleanupBareRepo();
237
- const httpsMessage = error instanceof Error ? error.message : String(error);
238
- const sshMessage = sshError instanceof Error ? sshError.message : String(sshError);
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ const altMessage = altError instanceof Error ? altError.message : String(altError);
239
244
  throw new Error(`Failed to clone ${owner}/${repo}. ` +
240
- `HTTPS では認証情報が無いため失敗し、SSH でも失敗しました。` +
241
- `SSH鍵(ssh-agent)を設定するか、リポジトリへのアクセス権を確認してください。\n` +
242
- `HTTPS: ${httpsMessage.trim()}\nSSH: ${sshMessage.trim()}`);
245
+ `HTTPS・SSH のどちらでも認証に失敗しました。` +
246
+ `SSH鍵(ssh-agent)または git の credential helper を設定するか、リポジトリへのアクセス権を確認してください。\n` +
247
+ `${protocol}: ${message.trim()}\n${altProtocol}: ${altMessage.trim()}`);
243
248
  }
244
249
  }
245
250
  const newRepo = await createRepo({ owner, repo, cloneUrl: clonedUrl });
246
251
  return reply.status(200).send({
247
252
  data: {
248
253
  repository: { ...newRepo, bareRepoPath },
249
- // HTTPSで認証できずSSHで clone し直した場合、UI側で利用者に知らせる
250
- fallbackToSsh: clonedUrl !== normalizedUrl,
254
+ // 認証できず別方式で clone し直した場合、UI側で利用者に知らせる
255
+ fallbackProtocol: clonedUrl !== normalizedUrl ? altProtocol.toLowerCase() : null,
251
256
  },
252
257
  });
253
258
  }
@@ -2,7 +2,13 @@ import { v4 as uuidv4 } from 'uuid';
2
2
  import * as taskRepo from '../lib/repositories/task.js';
3
3
  import * as worktreeManager from '../lib/worktree-manager.js';
4
4
  import { ptyManager } from '../pty-manager.js';
5
- import { listTasks, createTask, getTask, updateTask, deleteTask, TaskServiceError, } from '../lib/services/task-service.js';
5
+ import { listTasks, createTask, getTask, updateTask, deleteTask, validateNewTaskBranch, TaskServiceError, } from '../lib/services/task-service.js';
6
+ const CREATE_ERROR_STATUS = {
7
+ REPO_NOT_FOUND: 404,
8
+ BRANCH_DUPLICATE: 409,
9
+ INVALID_BRANCH: 400,
10
+ WORKTREE_CREATION_FAILED: 422,
11
+ };
6
12
  export async function tasksRoutes(fastify) {
7
13
  const io = fastify.io;
8
14
  // POST /tasks/validate
@@ -17,22 +23,16 @@ export async function tasksRoutes(fastify) {
17
23
  ],
18
24
  });
19
25
  }
20
- const existingTasks = await taskRepo.getTasks({ includeDeleted: false });
21
- const duplicateTask = existingTasks.find((task) => task.owner === owner && task.repo === repo && task.branch === branch);
22
- if (duplicateTask) {
23
- return reply.status(409).send({
24
- valid: false,
25
- errors: [
26
- {
27
- field: 'branch',
28
- message: `Branch "${branch}" already exists. Task "${duplicateTask.title}" (ID: ${duplicateTask.id}) is already using this branch.`,
29
- },
30
- ],
31
- });
32
- }
26
+ await validateNewTaskBranch(owner, repo, branch);
33
27
  return reply.status(200).send({ valid: true });
34
28
  }
35
29
  catch (error) {
30
+ if (error instanceof TaskServiceError) {
31
+ return reply.status(error.code === 'BRANCH_DUPLICATE' ? 409 : 400).send({
32
+ valid: false,
33
+ errors: [{ field: 'branch', message: error.message }],
34
+ });
35
+ }
36
36
  fastify.log.error(error, 'POST /tasks/validate error');
37
37
  return reply.status(500).send({
38
38
  valid: false,
@@ -128,11 +128,11 @@ export async function tasksRoutes(fastify) {
128
128
  }
129
129
  try {
130
130
  const result = await createTask({ title, description, owner, repo, branch, baseBranch, effort, order }, { io });
131
- return reply.status(201).send({ data: { task: result.task } });
131
+ return reply.status(201).send({ data: { task: result.task, warnings: result.warnings } });
132
132
  }
133
133
  catch (error) {
134
134
  if (error instanceof TaskServiceError) {
135
- const statusCode = error.code === 'REPO_NOT_FOUND' ? 404 : error.code === 'BRANCH_DUPLICATE' ? 409 : 500;
135
+ const statusCode = CREATE_ERROR_STATUS[error.code] ?? 500;
136
136
  return reply.status(statusCode).send({
137
137
  errors: [{ field: 'global', message: error.message }],
138
138
  });
@@ -146,7 +146,8 @@ export async function tasksRoutes(fastify) {
146
146
  try {
147
147
  const { id } = request.params;
148
148
  const task = await getTask({ id });
149
- return reply.status(200).send({ data: { task } });
149
+ const worktreeExists = await worktreeManager.worktreeExists(task.owner, task.repo, task.branch);
150
+ return reply.status(200).send({ data: { task, worktreeExists } });
150
151
  }
151
152
  catch (error) {
152
153
  if (error instanceof TaskServiceError && error.code === 'TASK_NOT_FOUND') {
@@ -243,10 +244,8 @@ export async function tasksRoutes(fastify) {
243
244
  if (!task) {
244
245
  return reply.status(404).send({ error: 'Task not found' });
245
246
  }
246
- let needsRebase = false;
247
- if (task.worktreeStatus === 'created') {
248
- needsRebase = await worktreeManager.checkRebaseNeeded(task.owner, task.repo, task.branch, task.baseBranch);
249
- }
247
+ // worktreeが存在しない場合は checkRebaseNeeded が false を返す
248
+ const needsRebase = await worktreeManager.checkRebaseNeeded(task.owner, task.repo, task.branch, task.baseBranch);
250
249
  return reply.status(200).send({ data: { needsRebase } });
251
250
  }
252
251
  catch (error) {
@@ -263,8 +262,8 @@ export async function tasksRoutes(fastify) {
263
262
  if (!task) {
264
263
  return reply.status(404).send({ error: 'Task not found' });
265
264
  }
266
- if (task.worktreeStatus !== 'created') {
267
- return reply.status(400).send({ error: 'Worktree has not been created yet' });
265
+ if (!(await worktreeManager.worktreeExists(task.owner, task.repo, task.branch))) {
266
+ return reply.status(400).send({ error: 'Worktree not found' });
268
267
  }
269
268
  const isClaudeRunning = task.tabs.some((tab) => tab.status === 'running');
270
269
  if (isClaudeRunning) {
@@ -227,11 +227,16 @@ export async function terminalRoutes(fastify) {
227
227
  let workingDir = tab
228
228
  ? getWorktreePath(tab.task.owner, tab.task.repo, tab.task.branch)
229
229
  : (cwd ?? defaultDir);
230
- // cwd が存在するか確認、なければデフォルトにフォールバック
230
+ // cwd が存在するか確認。Task のタブは worktree 必須(無ければ起動しない)、
231
+ // それ以外はデフォルトにフォールバック
231
232
  try {
232
233
  await fs.access(workingDir);
233
234
  }
234
235
  catch {
236
+ if (tab) {
237
+ fastify.log.warn({ sessionId, workingDir }, 'worktree does not exist');
238
+ return reply.status(409).send({ error: `Worktree not found at ${workingDir}` });
239
+ }
235
240
  fastify.log.warn({ sessionId, workingDir }, 'cwd does not exist, falling back to default');
236
241
  workingDir = defaultDir;
237
242
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@minimalcorp/tsunagi",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@minimalcorp/tsunagi",
9
- "version": "0.1.44",
9
+ "version": "0.1.46",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "os": [
12
12
  "darwin",
@@ -1500,9 +1500,9 @@
1500
1500
  }
1501
1501
  },
1502
1502
  "node_modules/bundle-name": {
1503
- "version": "4.1.0",
1504
- "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
1505
- "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
1503
+ "version": "4.1.1",
1504
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
1505
+ "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
1506
1506
  "license": "MIT",
1507
1507
  "dependencies": {
1508
1508
  "run-applescript": "^7.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minimalcorp/tsunagi",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Multi-repo GitHub project management with Claude AI integration, designed for visualizing and controlling AI-driven development locally.",
5
5
  "keywords": [
6
6
  "tsunagi",