Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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/ を使う
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ workspace/
.claude/tmp/
.gstack/

# superpowers の中間 planning doc。実装と不整合化しやすく review noise になるため untrack。
docs/superpowers/

# Playwright
packages/*/.playwright-tally-home/
packages/*/playwright-report/
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 の「ストーリー分解」ボタンを押下
Expand Down
6 changes: 3 additions & 3 deletions docs/03-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand Down Expand Up @@ -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 # レジストリ・デフォルトプロジェクト置き場(省略時はこの値)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/04-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

- `pnpm install` がエラーなく完了
- `pnpm -r test` が通る(空のテストでOK)
- `pnpm --filter frontend dev` で http://localhost:3000 が表示される
- `pnpm --filter frontend dev` で http://localhost:3321 が表示される

---

Expand Down
2 changes: 1 addition & 1 deletion examples/sample-project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ sample-project/
pnpm dev

# ブラウザで以下を開く
# http://localhost:3000/projects/taskflow-invite
# http://localhost:3321/projects/taskflow-invite
```

## 注意
Expand Down
4 changes: 2 additions & 2 deletions packages/ai-engine/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 を解釈する', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/ai-engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
4 changes: 2 additions & 2 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/lib/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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[] = [];
Expand Down
30 changes: 20 additions & 10 deletions packages/storage/src/chat-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions packages/storage/src/yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
22 changes: 22 additions & 0 deletions packages/storage/src/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function serializePreservingComments(doc: Document, data: Record<string, unknown
if (!isMap(contents)) {
// top-level が Map でない (空 Document など) → 全面書き換え
doc.contents = doc.createNode(data) as typeof doc.contents;
forceBlockStyle(doc.contents);
return String(doc);
}

Expand All @@ -98,11 +99,27 @@ function serializePreservingComments(doc: Document, data: Record<string, unknown
mergeSeqById(existingValue, v, doc);
} else {
doc.set(k, v);
forceBlockStyle(contents.get(k, true));
}
}
return String(doc);
}

// 空コレクション (`messages: []`) を初回書き込みで保存すると yaml lib は flow style
// `[]` で出力し、`flow=true` フラグを保持したまま次の書き込みでも引き継ぐ。
// その flow seq の中に複数行 string が入った map を追加すると、
// 「フロー集約の中にブロック scalar」が混在して再パース不能な YAML になる。
// 既存の Pair / value に付くコメントは残したままで flow フラグだけ落とす。
function forceBlockStyle(node: unknown): void {
if (isSeq(node) || isMap(node)) {
(node as YAMLSeq | YAMLMap).flow = false;
for (const item of (node as YAMLSeq | YAMLMap).items) {
if (isPair(item)) forceBlockStyle(item.value);
else forceBlockStyle(item);
}
}
}

function isArrayOfIdObjects(
arr: unknown[],
): arr is Array<Record<string, unknown> & { id: string }> {
Expand All @@ -124,6 +141,9 @@ function mergeSeqById(
data: Array<Record<string, unknown> & { id: string }>,
doc: Document,
): void {
// 既存 seq が flow style (`[]` で初期化された空配列由来) のままだと、
// 中に追加する map が flow になり複数行 string と相性が悪い。block 強制。
seq.flow = false;
// 既存アイテムを id で引ける Map にする
const existingById = new Map<string, YAMLMap>();
for (const item of seq.items) {
Expand All @@ -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);
}
}
Expand Down