diff --git a/.env.example b/.env.example index bc2f3c7..e3f6812 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ -# Tally AI Engine の WebSocket ポート (任意、デフォルト 4000) -AI_ENGINE_PORT=4000 +# Tally AI Engine の WebSocket ポート (任意、デフォルト 3322) +# 4000/4001/5050 等は他プロジェクトと衝突しがちなので避ける。 +AI_ENGINE_PORT=3322 -# フロントエンドが AI Engine に接続する URL (任意、デフォルト ws://localhost:4000) -# NEXT_PUBLIC_AI_ENGINE_URL=ws://localhost:4000 +# フロントエンドが AI Engine に接続する URL (任意、デフォルト ws://localhost:3322) +# NEXT_PUBLIC_AI_ENGINE_URL=ws://localhost:3322 # プロジェクトデータの保存先 (開発用) # 実運用では対象リポジトリ配下の .tally/ を使う diff --git a/.gitignore b/.gitignore index 3afc9cc..e379075 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ workspace/ .claude/tmp/ .gstack/ +# superpowers の中間 planning doc。実装と不整合化しやすく review noise になるため untrack。 +docs/superpowers/ + # Playwright packages/*/.playwright-tally-home/ packages/*/playwright-report/ diff --git a/README.md b/README.md index 79b5d82..044ab43 100644 --- a/README.md +++ b/README.md @@ -65,14 +65,14 @@ cp .env.example .env # 必要なら編集 pnpm dev ``` -- frontend: http://localhost:3000 -- ai-engine: ws://localhost:4000/agent +- frontend: http://localhost:3321 +- ai-engine: ws://localhost:3322/agent `pnpm dev` は `pnpm -r --parallel dev` を呼び、frontend (Next.js dev) と ai-engine (tsx watch) を並列起動する。 ### 利用フロー (要点) -1. ブラウザで http://localhost:3000 を開く +1. ブラウザで http://localhost:3321 を開く 2. 「+ 新規プロジェクト」でフォルダ選択ダイアログから保存先を選ぶ(`~/.local/share/tally/projects/<名前>/` が提案される) 3. 任意で 1 つ以上の「コードベース」(AI が探索する対象リポジトリ)を追加して「作成」 4. UC ノードを選択 → DetailSheet の「ストーリー分解」ボタンを押下 diff --git a/docs/03-architecture.md b/docs/03-architecture.md index 40dcd8b..01b79ba 100644 --- a/docs/03-architecture.md +++ b/docs/03-architecture.md @@ -137,8 +137,8 @@ User taps "UC" node → "ストーリー分解" ``` $ pnpm dev -├── frontend (Next.js dev server, :3000) -├── ai-engine (WebSocket server, :3001) +├── frontend (Next.js dev server, :3321) +├── ai-engine (WebSocket server, :3322) └── storage (inline in Next.js Route Handlers) ``` @@ -173,7 +173,7 @@ AI Engine を別プロセスにする理由: ``` ANTHROPIC_API_KEY=sk-ant-... # Claude Agent SDK -TALLY_AI_PORT=3001 # AI Engine WebSocket ポート +AI_ENGINE_PORT=3322 # AI Engine WebSocket ポート (env 名は ai-engine の loadConfig に揃える) TALLY_HOME=~/.local/share/tally # レジストリ・デフォルトプロジェクト置き場(省略時はこの値) ``` diff --git a/docs/04-roadmap.md b/docs/04-roadmap.md index 3a51bf1..fd7204d 100644 --- a/docs/04-roadmap.md +++ b/docs/04-roadmap.md @@ -21,7 +21,7 @@ - `pnpm install` がエラーなく完了 - `pnpm -r test` が通る(空のテストでOK) -- `pnpm --filter frontend dev` で http://localhost:3000 が表示される +- `pnpm --filter frontend dev` で http://localhost:3321 が表示される --- diff --git a/examples/sample-project/README.md b/examples/sample-project/README.md index 0a2417c..cf6162f 100644 --- a/examples/sample-project/README.md +++ b/examples/sample-project/README.md @@ -41,7 +41,7 @@ sample-project/ pnpm dev # ブラウザで以下を開く -# http://localhost:3000/projects/taskflow-invite +# http://localhost:3321/projects/taskflow-invite ``` ## 注意 diff --git a/packages/ai-engine/src/config.test.ts b/packages/ai-engine/src/config.test.ts index 5b941cc..b682bab 100644 --- a/packages/ai-engine/src/config.test.ts +++ b/packages/ai-engine/src/config.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'; import { loadConfig } from './config'; describe('loadConfig', () => { - it('デフォルト PORT は 4000', () => { + it('デフォルト PORT は 3322 (3321 frontend の隣、他プロジェクトと衝突しがちな 3000/4000/4001/5050 を避ける)', () => { const cfg = loadConfig({}); - expect(cfg.port).toBe(4000); + expect(cfg.port).toBe(3322); }); it('AI_ENGINE_PORT を解釈する', () => { diff --git a/packages/ai-engine/src/config.ts b/packages/ai-engine/src/config.ts index 43ce3dc..cd26d9f 100644 --- a/packages/ai-engine/src/config.ts +++ b/packages/ai-engine/src/config.ts @@ -6,7 +6,9 @@ export interface AiEngineConfig { // 認証情報は扱わない (Claude Code OAuth トークンを SDK が暗黙で拾う)。 export function loadConfig(env: NodeJS.ProcessEnv): AiEngineConfig { const raw = env.AI_ENGINE_PORT; - if (raw === undefined || raw === '') return { port: 4000 }; + // default 3322: 3321 (frontend) の隣。3000/4000/4001/5050 等は他プロジェクトと衝突しがち。 + // env AI_ENGINE_PORT で上書き可能。 + if (raw === undefined || raw === '') return { port: 3322 }; const n = Number.parseInt(raw, 10); if (!Number.isFinite(n) || n <= 0) { throw new Error(`AI_ENGINE_PORT が不正: ${raw}`); diff --git a/packages/frontend/README.md b/packages/frontend/README.md index ef45ca7..3de95fc 100644 --- a/packages/frontend/README.md +++ b/packages/frontend/README.md @@ -59,7 +59,7 @@ src/ ## 開発 ```bash -pnpm --filter @tally/frontend dev # http://localhost:3000 +pnpm --filter @tally/frontend dev # http://localhost:3321 pnpm --filter @tally/frontend build pnpm --filter @tally/frontend typecheck ``` diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 800a9bf..54e8970 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,9 +4,9 @@ "private": true, "description": "Tally のフロントエンド (Next.js 16 App Router + React Flow キャンバス)", "scripts": { - "dev": "next dev", + "dev": "next dev -p ${PORT:-3321}", "build": "next build", - "start": "next start", + "start": "next start -p ${PORT:-3321}", "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", diff --git a/packages/frontend/playwright.config.ts b/packages/frontend/playwright.config.ts index 97a731f..b27f84a 100644 --- a/packages/frontend/playwright.config.ts +++ b/packages/frontend/playwright.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ globalSetup: './e2e/global-setup.ts', use: { - baseURL: 'http://localhost:3000', + baseURL: 'http://localhost:3321', trace: 'retain-on-failure', screenshot: 'only-on-failure', }, @@ -31,7 +31,7 @@ export default defineConfig({ // frontend の dev server を自動起動。ai-engine は未起動でもノード表示は動く (chat を開かない限り WS 接続なし)。 webServer: { command: 'pnpm dev', - url: 'http://localhost:3000', + url: 'http://localhost:3321', reuseExistingServer: !process.env.CI, timeout: 120_000, env: { diff --git a/packages/frontend/src/lib/ws.ts b/packages/frontend/src/lib/ws.ts index 032b208..c5dc556 100644 --- a/packages/frontend/src/lib/ws.ts +++ b/packages/frontend/src/lib/ws.ts @@ -20,7 +20,7 @@ export interface AgentHandle { // WS ベースの agent 呼び出し。受信した NDJSON を AgentEvent の AsyncIterable に変換する。 // close() で接続を明示的に終わらせる。サーバ側が close したら AsyncIterable も終了する。 export function startAgent(opts: StartAgentOptions): AgentHandle { - const url = opts.url ?? process.env.NEXT_PUBLIC_AI_ENGINE_URL ?? 'ws://localhost:4000'; + const url = opts.url ?? process.env.NEXT_PUBLIC_AI_ENGINE_URL ?? 'ws://localhost:3322'; const ws = new WebSocket(`${url}/agent`); const buf: AgentEvent[] = []; @@ -107,7 +107,7 @@ export interface OpenChatOptions { // sendUserMessage / approveTool を任意のタイミングで呼ぶ。 // サーバが close した場合は events も終了する。 export function openChat(opts: OpenChatOptions): ChatHandle { - const url = opts.url ?? process.env.NEXT_PUBLIC_AI_ENGINE_URL ?? 'ws://localhost:4000'; + const url = opts.url ?? process.env.NEXT_PUBLIC_AI_ENGINE_URL ?? 'ws://localhost:3322'; const ws = new WebSocket(`${url}/chat`); const buf: ChatEvent[] = []; diff --git a/packages/storage/src/chat-store.ts b/packages/storage/src/chat-store.ts index 56bf4c5..a0a945d 100644 --- a/packages/storage/src/chat-store.ts +++ b/packages/storage/src/chat-store.ts @@ -12,7 +12,7 @@ import { } from '@tally/core'; import { chatFileName, resolveProjectPaths } from './project-dir'; -import { readYaml, writeYaml } from './yaml'; +import { readYaml, writeYaml, YamlValidationError } from './yaml'; export interface CreateChatInput { projectId: string; @@ -91,15 +91,25 @@ export class FileSystemChatStore implements ChatStore { const yamlFiles = entries.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); const threads = await Promise.all( yamlFiles.map(async (file) => { - const t = await readYaml(path.join(this.paths.chatsDir, file), ChatThreadSchema); - if (!t) return null; - return { - id: t.id, - projectId: t.projectId, - title: t.title, - createdAt: t.createdAt, - updatedAt: t.updatedAt, - } satisfies ChatThreadMeta; + // YAML パース/スキーマ違反のみ skip 対象。FS 系 IO エラー (EACCES / EMFILE 等) + // は黙って隠蔽すると問題発見が遅れるため再スローして 500 に倒す。 + try { + const t = await readYaml(path.join(this.paths.chatsDir, file), ChatThreadSchema); + if (!t) return null; + return { + id: t.id, + projectId: t.projectId, + title: t.title, + createdAt: t.createdAt, + updatedAt: t.updatedAt, + } satisfies ChatThreadMeta; + } catch (err) { + if (err instanceof YamlValidationError) { + console.warn(`[chat-store] skip broken chat file: ${file}`, err); + return null; + } + throw err; + } }), ); return threads diff --git a/packages/storage/src/yaml.test.ts b/packages/storage/src/yaml.test.ts index 8eb483d..7f95384 100644 --- a/packages/storage/src/yaml.test.ts +++ b/packages/storage/src/yaml.test.ts @@ -148,6 +148,44 @@ name: old }); }); + describe('writeYaml flow→block 強制', () => { + // regression: 空配列 (`messages: []`) を初回書き込みすると yaml lib は flow style で + // 出力する。次回書き込み時、既存 seq が flow=true のまま map を追加すると + // 「フロー集約の中にブロック scalar」が混在し再パースが破綻していた (chat YAML 破損)。 + it('id 配列に複数行 string を含む要素を追加しても再パース可能な YAML が出る', async () => { + const filePath = path.join(workspace, 'messages.yaml'); + // Step 1: 空配列で初回書き込み (yaml lib が `messages: []` flow style で書き出す) + await writeYaml(filePath, { + id: 'thread-1', + messages: [], + }); + // Step 2: 複数行 string を含む要素を追加 + await writeYaml(filePath, { + id: 'thread-1', + messages: [ + { + id: 'msg-1', + text: 'line one\n\nline two\n\nline three', + }, + ], + }); + // 再パースできれば fix 成立 + const re = await readYaml( + filePath, + z.object({ + id: z.string(), + messages: z.array(z.object({ id: z.string(), text: z.string() })), + }), + ); + expect(re?.messages).toHaveLength(1); + expect(re?.messages[0]?.text).toBe('line one\n\nline two\n\nline three'); + // 出力は block style (`- id: msg-1`) になっているべき + const raw = await fs.readFile(filePath, 'utf8'); + expect(raw).toContain('- id: msg-1'); + expect(raw).not.toMatch(/messages:\s*\[/); + }); + }); + describe('writeYaml + readYaml 往復', () => { it('書いて読み直せば元のデータと一致する', async () => { const filePath = path.join(workspace, 'roundtrip.yaml'); diff --git a/packages/storage/src/yaml.ts b/packages/storage/src/yaml.ts index 8cd53f3..2f6180b 100644 --- a/packages/storage/src/yaml.ts +++ b/packages/storage/src/yaml.ts @@ -78,6 +78,7 @@ function serializePreservingComments(doc: Document, data: Record & { id: string }> { @@ -124,6 +141,9 @@ function mergeSeqById( data: Array & { id: string }>, doc: Document, ): void { + // 既存 seq が flow style (`[]` で初期化された空配列由来) のままだと、 + // 中に追加する map が flow になり複数行 string と相性が悪い。block 強制。 + seq.flow = false; // 既存アイテムを id で引ける Map にする const existingById = new Map(); for (const item of seq.items) { @@ -140,9 +160,11 @@ function mergeSeqById( const existing = existingById.get(obj.id); if (existing) { updateMapInPlace(existing, obj, doc); + forceBlockStyle(existing); nextItems.push(existing); } else { const newNode = doc.createNode(obj); + forceBlockStyle(newNode); nextItems.push(newNode as YamlNode); } }