From ba8e90318c59db6501ba92aea0cf9229f8be2c39 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:33:22 +0900 Subject: [PATCH 1/5] feat(builtins): add API client generation consistency rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生成クライアント(Orval等)が存在するプロジェクトで手書きAPI呼び出しとの混在を検出するナレッジとポリシーを追加 --- builtins/ja/knowledge/frontend.md | 15 +++++++++++++++ builtins/ja/policies/ai-antipattern.md | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/builtins/ja/knowledge/frontend.md b/builtins/ja/knowledge/frontend.md index 621a437..6051c09 100644 --- a/builtins/ja/knowledge/frontend.md +++ b/builtins/ja/knowledge/frontend.md @@ -156,6 +156,21 @@ const Parent = () => { | 複数コンポーネントで共有 | Context or 状態管理ライブラリ | | サーバーデータのキャッシュ | TanStack Query等のデータフェッチライブラリ | +## APIクライアント生成 + +プロジェクトがAPIクライアント生成ツール(Orval、openapi-typescript等)を採用している場合、新規APIエンドポイントとの接続には必ず生成されたクライアントを使用する。 + +| パターン | 判定 | +|---------|------| +| 生成ツールが存在するのに axiosInstance/fetch を直接使用 | REJECT | +| 生成ツールの設定を確認せずにAPIフックを手書き | REJECT | +| 生成ツールが存在しないプロジェクトで直接呼び出し | OK | + +確認手順: +1. プロジェクトにAPI生成設定があるか確認(orval.config.ts, openapi-generator 等) +2. 既存の生成済みクライアントの使用パターンを確認 +3. 新規エンドポイントは生成パイプラインに追加し、生成されたフックを使う + ## データ取得 API呼び出しはルート(View)コンポーネントで行い、子コンポーネントにはpropsで渡す。 diff --git a/builtins/ja/policies/ai-antipattern.md b/builtins/ja/policies/ai-antipattern.md index e8b2ee0..ae8c1aa 100644 --- a/builtins/ja/policies/ai-antipattern.md +++ b/builtins/ja/policies/ai-antipattern.md @@ -63,6 +63,22 @@ AIは同じパターンを、間違いも含めて繰り返すことが多い。 - ここに属しているように感じるか? - プロジェクト規則からの説明のない逸脱はないか? +## インテグレーションパターンの一貫性 + +同じ種類のAPI接続(REST呼び出し等)がプロジェクト内で異なる方式で実装されていないか確認する。 + +| パターン | 例 | 判定 | +|---------|-----|------| +| 生成クライアントと手書きクライアントの混在 | A画面はOrval生成フック、B画面はaxiosInstance直接 | REJECT | +| 同じデータ取得パターンの異なる実装 | A画面はuseQuery+axios、B画面は生成フック | REJECT | +| データ型の定義方式の混在 | A画面は生成された型、B画面は手書きの型 | REJECT | + +検証アプローチ: +1. 変更差分のAPI呼び出し方式を確認 +2. 同じ目的の既存コードがどの方式で書かれているか grep で確認 +3. プロジェクトにAPI生成設定(orval.config.ts等)があるか確認 +4. 不整合がある場合、プロジェクトの標準パターンへの統一を指摘する + ## スコープクリープ検出 AIは過剰に提供する傾向がある。不要な追加をチェック。 From dd58783f5ee730a51ae59e36901038ca07e4917b Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:56:33 +0900 Subject: [PATCH 2/5] refactor(e2e): extract shared vitest config and add forceExit to prevent zombie workers --- vitest.config.e2e.base.ts | 16 ++++++++++++++++ vitest.config.e2e.mock.ts | 13 ++----------- vitest.config.e2e.provider.ts | 13 ++----------- vitest.config.e2e.structured-output.ts | 13 ++----------- vitest.config.e2e.ts | 13 ++----------- 5 files changed, 24 insertions(+), 44 deletions(-) create mode 100644 vitest.config.e2e.base.ts diff --git a/vitest.config.e2e.base.ts b/vitest.config.e2e.base.ts new file mode 100644 index 0000000..4e0f413 --- /dev/null +++ b/vitest.config.e2e.base.ts @@ -0,0 +1,16 @@ +import { UserConfig } from 'vitest/config'; + +export const e2eBaseTestConfig: UserConfig['test'] = { + environment: 'node', + globals: false, + testTimeout: 240000, + hookTimeout: 60000, + teardownTimeout: 30000, + forceExit: true, + pool: 'threads', + poolOptions: { + threads: { + singleThread: true, + }, + }, +}; diff --git a/vitest.config.e2e.mock.ts b/vitest.config.e2e.mock.ts index 3d6abf5..b9826e5 100644 --- a/vitest.config.e2e.mock.ts +++ b/vitest.config.e2e.mock.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config'; +import { e2eBaseTestConfig } from './vitest.config.e2e.base'; export default defineConfig({ test: { + ...e2eBaseTestConfig, include: [ 'e2e/specs/direct-task.e2e.ts', 'e2e/specs/pipeline-skip-git.e2e.ts', @@ -34,16 +36,5 @@ export default defineConfig({ 'e2e/specs/quiet-mode.e2e.ts', 'e2e/specs/task-content-file.e2e.ts', ], - environment: 'node', - globals: false, - testTimeout: 240000, - hookTimeout: 60000, - teardownTimeout: 30000, - pool: 'threads', - poolOptions: { - threads: { - singleThread: true, - }, - }, }, }); diff --git a/vitest.config.e2e.provider.ts b/vitest.config.e2e.provider.ts index cd00e37..a35d436 100644 --- a/vitest.config.e2e.provider.ts +++ b/vitest.config.e2e.provider.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config'; +import { e2eBaseTestConfig } from './vitest.config.e2e.base'; export default defineConfig({ test: { + ...e2eBaseTestConfig, include: [ 'e2e/specs/add-and-run.e2e.ts', 'e2e/specs/worktree.e2e.ts', @@ -9,16 +11,5 @@ export default defineConfig({ 'e2e/specs/github-issue.e2e.ts', 'e2e/specs/structured-output.e2e.ts', ], - environment: 'node', - globals: false, - testTimeout: 240000, - hookTimeout: 60000, - teardownTimeout: 30000, - pool: 'threads', - poolOptions: { - threads: { - singleThread: true, - }, - }, }, }); diff --git a/vitest.config.e2e.structured-output.ts b/vitest.config.e2e.structured-output.ts index 9926aa5..fb98f6c 100644 --- a/vitest.config.e2e.structured-output.ts +++ b/vitest.config.e2e.structured-output.ts @@ -1,20 +1,11 @@ import { defineConfig } from 'vitest/config'; +import { e2eBaseTestConfig } from './vitest.config.e2e.base'; export default defineConfig({ test: { + ...e2eBaseTestConfig, include: [ 'e2e/specs/structured-output.e2e.ts', ], - environment: 'node', - globals: false, - testTimeout: 240000, - hookTimeout: 60000, - teardownTimeout: 30000, - pool: 'threads', - poolOptions: { - threads: { - singleThread: true, - }, - }, }, }); diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index e3c715f..63ee90e 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -1,18 +1,9 @@ import { defineConfig } from 'vitest/config'; +import { e2eBaseTestConfig } from './vitest.config.e2e.base'; export default defineConfig({ test: { + ...e2eBaseTestConfig, include: ['e2e/specs/**/*.e2e.ts'], - environment: 'node', - globals: false, - testTimeout: 240000, - hookTimeout: 60000, - teardownTimeout: 30000, - pool: 'threads', - poolOptions: { - threads: { - singleThread: true, - }, - }, }, }); From 89cb3f8dbf59a2c3d93ead3654a681515e4ee6bb Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:02:17 +0900 Subject: [PATCH 3/5] fix(task-store): prevent EPERM crash in lock release by tracking ownership in memory --- src/infra/task/store.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/infra/task/store.ts b/src/infra/task/store.ts index ea6329c..6163204 100644 --- a/src/infra/task/store.ts +++ b/src/infra/task/store.ts @@ -13,10 +13,15 @@ function sleepSync(ms: number): void { Atomics.wait(arr, 0, 0, ms); } +function fsErrorCode(err: unknown): string | undefined { + return (err as NodeJS.ErrnoException).code; +} + export class TaskStore { private readonly tasksFile: string; private readonly lockFile: string; private readonly taktDir: string; + private lockOwned = false; constructor(private readonly projectDir: string) { this.taktDir = path.join(projectDir, '.takt'); @@ -94,10 +99,10 @@ export class TaskStore { while (true) { try { fs.writeFileSync(this.lockFile, String(process.pid), { encoding: 'utf-8', flag: 'wx' }); + this.lockOwned = true; return; } catch (err) { - const nodeErr = err as NodeJS.ErrnoException; - if (nodeErr.code !== 'EEXIST') { + if (fsErrorCode(err) !== 'EEXIST') { throw err; } } @@ -120,8 +125,8 @@ export class TaskStore { try { pidRaw = fs.readFileSync(this.lockFile, 'utf-8').trim(); } catch (err) { - const nodeErr = err as NodeJS.ErrnoException; - if (nodeErr.code === 'ENOENT') { + const code = fsErrorCode(err); + if (code === 'ENOENT' || code === 'EPERM') { return false; } throw err; @@ -139,8 +144,7 @@ export class TaskStore { try { fs.unlinkSync(this.lockFile); } catch (err) { - const nodeErr = err as NodeJS.ErrnoException; - if (nodeErr.code !== 'ENOENT') { + if (fsErrorCode(err) !== 'ENOENT') { log.debug('Failed to remove stale lock, retrying.', { lockFile: this.lockFile, error: String(err) }); } } @@ -151,11 +155,11 @@ export class TaskStore { process.kill(pid, 0); return true; } catch (err) { - const nodeErr = err as NodeJS.ErrnoException; - if (nodeErr.code === 'ESRCH') { + const code = fsErrorCode(err); + if (code === 'ESRCH') { return false; } - if (nodeErr.code === 'EPERM') { + if (code === 'EPERM') { return true; } throw err; @@ -163,19 +167,14 @@ export class TaskStore { } private releaseLock(): void { + if (!this.lockOwned) return; + this.lockOwned = false; + try { - const holder = fs.readFileSync(this.lockFile, 'utf-8').trim(); - if (holder !== String(process.pid)) { - return; - } fs.unlinkSync(this.lockFile); } catch (err) { - const nodeErr = err as NodeJS.ErrnoException; - if (nodeErr.code === 'ENOENT') { - return; - } + if (fsErrorCode(err) === 'ENOENT') return; log.debug('Failed to release tasks lock.', { lockFile: this.lockFile, error: String(err) }); - throw err; } } } From 251acf8e518b24fa034f17e79ef0a91e68acfc21 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:14:29 +0900 Subject: [PATCH 4/5] refactor(task-store): replace file-based lock with in-memory guard --- src/__tests__/task.test.ts | 28 +--------- src/infra/task/store.ts | 108 +++---------------------------------- 2 files changed, 7 insertions(+), 129 deletions(-) diff --git a/src/__tests__/task.test.ts b/src/__tests__/task.test.ts index 1cf5e99..1005d72 100644 --- a/src/__tests__/task.test.ts +++ b/src/__tests__/task.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; @@ -94,32 +94,6 @@ describe('TaskRunner (tasks.yaml)', () => { expect(recovered).toBe(0); }); - it('should take over stale lock file with invalid pid', () => { - mkdirSync(join(testDir, '.takt'), { recursive: true }); - writeFileSync(join(testDir, '.takt', 'tasks.yaml.lock'), 'invalid-pid', 'utf-8'); - - const task = runner.addTask('Task with stale lock'); - - expect(task.name).toContain('task-with-stale-lock'); - expect(existsSync(join(testDir, '.takt', 'tasks.yaml.lock'))).toBe(false); - }); - - it('should timeout when lock file is held by a live process', () => { - mkdirSync(join(testDir, '.takt'), { recursive: true }); - writeFileSync(join(testDir, '.takt', 'tasks.yaml.lock'), String(process.pid), 'utf-8'); - - const dateNowSpy = vi.spyOn(Date, 'now'); - dateNowSpy.mockReturnValueOnce(0); - dateNowSpy.mockReturnValue(5_000); - - try { - expect(() => runner.listTasks()).toThrow('Failed to acquire tasks lock within 5000ms'); - } finally { - dateNowSpy.mockRestore(); - rmSync(join(testDir, '.takt', 'tasks.yaml.lock'), { force: true }); - } - }); - it('should recover from corrupted tasks.yaml and allow adding tasks again', () => { mkdirSync(join(testDir, '.takt'), { recursive: true }); writeFileSync(join(testDir, '.takt', 'tasks.yaml'), 'tasks:\n - name: [broken', 'utf-8'); diff --git a/src/infra/task/store.ts b/src/infra/task/store.ts index 6163204..0e268f4 100644 --- a/src/infra/task/store.ts +++ b/src/infra/task/store.ts @@ -5,28 +5,15 @@ import { TasksFileSchema, type TasksFileData } from './schema.js'; import { createLogger } from '../../shared/utils/index.js'; const log = createLogger('task-store'); -const LOCK_WAIT_MS = 5_000; -const LOCK_POLL_MS = 50; - -function sleepSync(ms: number): void { - const arr = new Int32Array(new SharedArrayBuffer(4)); - Atomics.wait(arr, 0, 0, ms); -} - -function fsErrorCode(err: unknown): string | undefined { - return (err as NodeJS.ErrnoException).code; -} export class TaskStore { private readonly tasksFile: string; - private readonly lockFile: string; private readonly taktDir: string; - private lockOwned = false; + private locked = false; constructor(private readonly projectDir: string) { this.taktDir = path.join(projectDir, '.takt'); this.tasksFile = path.join(this.taktDir, 'tasks.yaml'); - this.lockFile = path.join(this.taktDir, 'tasks.yaml.lock'); } getTasksFilePath(): string { @@ -84,97 +71,14 @@ export class TaskStore { } private withLock(fn: () => T): T { - this.acquireLock(); + if (this.locked) { + throw new Error('TaskStore: reentrant lock detected'); + } + this.locked = true; try { return fn(); } finally { - this.releaseLock(); - } - } - - private acquireLock(): void { - this.ensureDirs(); - const start = Date.now(); - - while (true) { - try { - fs.writeFileSync(this.lockFile, String(process.pid), { encoding: 'utf-8', flag: 'wx' }); - this.lockOwned = true; - return; - } catch (err) { - if (fsErrorCode(err) !== 'EEXIST') { - throw err; - } - } - - if (this.isStaleLock()) { - this.removeStaleLock(); - continue; - } - - if (Date.now() - start >= LOCK_WAIT_MS) { - throw new Error(`Failed to acquire tasks lock within ${LOCK_WAIT_MS}ms`); - } - - sleepSync(LOCK_POLL_MS); - } - } - - private isStaleLock(): boolean { - let pidRaw: string; - try { - pidRaw = fs.readFileSync(this.lockFile, 'utf-8').trim(); - } catch (err) { - const code = fsErrorCode(err); - if (code === 'ENOENT' || code === 'EPERM') { - return false; - } - throw err; - } - - const pid = Number.parseInt(pidRaw, 10); - if (!Number.isInteger(pid) || pid <= 0) { - return true; - } - - return !this.isProcessAlive(pid); - } - - private removeStaleLock(): void { - try { - fs.unlinkSync(this.lockFile); - } catch (err) { - if (fsErrorCode(err) !== 'ENOENT') { - log.debug('Failed to remove stale lock, retrying.', { lockFile: this.lockFile, error: String(err) }); - } - } - } - - private isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch (err) { - const code = fsErrorCode(err); - if (code === 'ESRCH') { - return false; - } - if (code === 'EPERM') { - return true; - } - throw err; - } - } - - private releaseLock(): void { - if (!this.lockOwned) return; - this.lockOwned = false; - - try { - fs.unlinkSync(this.lockFile); - } catch (err) { - if (fsErrorCode(err) === 'ENOENT') return; - log.debug('Failed to release tasks lock.', { lockFile: this.lockFile, error: String(err) }); + this.locked = false; } } } From 8c9fe0e408ca56a13f99a30dfa1173780e16b9d1 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:08:25 +0900 Subject: [PATCH 5/5] Release v0.17.3 --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaa550..d9c7cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.17.3] - 2026-02-16 + +### Added + +- ビルトインの AI アンチパターンポリシーとフロントエンドナレッジに API クライアント生成の一貫性ルールを追加 — 生成ツール(Orval 等)が存在するプロジェクトでの手書きクライアント混在を検出 + +### Fixed + +- タスクストアのロック解放時に EPERM クラッシュが発生する問題を修正 — ファイルベースロックからインメモリガードに置き換え + +### Internal + +- e2e テストの vitest 設定を共通化し、forceExit オプション追加でゾンビワーカーを防止 + ## [0.17.2] - 2026-02-15 ### Added diff --git a/package-lock.json b/package-lock.json index 6a506ed..0e19b83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "takt", - "version": "0.17.2", + "version": "0.17.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "takt", - "version": "0.17.2", + "version": "0.17.3", "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.37", diff --git a/package.json b/package.json index d8c9782..114afb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "takt", - "version": "0.17.2", + "version": "0.17.3", "description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration", "main": "dist/index.js", "types": "dist/index.d.ts",