@ronkovic/aad 0.3.7 → 0.3.9

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/README.md CHANGED
@@ -5,19 +5,25 @@ TypeScript/Bun製マルチエージェント開発オーケストレーター。
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](https://www.typescriptlang.org/)
7
7
  [![Bun](https://img.shields.io/badge/Bun-1.2+-black)](https://bun.sh/)
8
- [![Tests](https://img.shields.io/badge/tests-738%2B%20passing-brightgreen)]()
9
- [![Version](https://img.shields.io/badge/version-0.3.0-orange)]()
8
+ [![npm](https://img.shields.io/npm/v/@ronkovic/aad)](https://www.npmjs.com/package/@ronkovic/aad)
9
+ [![Tests](https://img.shields.io/badge/tests-913%20passing-brightgreen)]()
10
+ [![Version](https://img.shields.io/badge/version-0.3.7-orange)]()
10
11
 
11
12
  ## 主要機能
12
13
 
13
14
  - **マルチエージェント並列実行**: 複数のClaudeエージェントが独立したタスクを並列処理
14
15
  - **TDDパイプライン**: Red → Green → Verify → Review → Merge の自動化されたテスト駆動開発
15
- - **マルチリポジトリ対応**: 複数リポジトリにまたがるタスクの統合管理 *(v0.3.0)*
16
- - **プラグインシステム**: カスタムアダプター・パイプラインステップの拡張 *(v0.3.0)*
16
+ - **インタラクティブモード**: `aad` で対話型ウィザードを起動、認証・プロジェクト設定を簡単に *(v0.3.6)*
17
+ - **セルフアップデート**: `aad update` で最新版に自動更新 *(v0.3.6)*
18
+ - **スマートブランチ命名**: 要件ドキュメントから自動でconventional commit prefix付きブランチ名を生成 *(v0.3.7)*
19
+ - **適応的エフォート調整**: タスク複雑度に応じてClaudeのeffort levelを自動調整
20
+ - **マルチリポジトリ対応**: 複数リポジトリにまたがるタスクの統合管理
21
+ - **プラグインシステム**: カスタムアダプター・パイプラインステップの拡張
17
22
  - **Git Workspace管理**: タスクごとに独立したworktreeを自動作成・管理
18
23
  - **依存関係解決**: タスク間の依存関係を自動検出し、適切な順序で実行
19
24
  - **リアルタイム監視**: Webダッシュボードで進捗・ログをリアルタイム表示
20
25
  - **構造化ログ**: pinoによる構造化ログで詳細なトレーシングが可能
26
+ - **メモリ安全機構**: OOMを防止する自動メモリゲート・シーケンシャルフォールバック
21
27
 
22
28
  ## アーキテクチャ
23
29
 
@@ -129,26 +135,34 @@ bun run dev
129
135
  ### 2. タスク実行
130
136
 
131
137
  ```bash
138
+ # 直接実行
132
139
  aad run requirements.md --workers 4
140
+
141
+ # インタラクティブモード(対話型ウィザード)
142
+ aad
133
143
  ```
134
144
 
135
145
  AADが自動的に:
136
- 1. タスクを分解・依存関係を解析
137
- 2. 各タスク用のworktreeを作成
138
- 3. 4つのワーカーで並列実行 (TDD: テスト作成 → 実装 → 検証 → レビュー)
139
- 4. 親ブランチへのマージ
146
+ 1. 要件ドキュメントからfeature名を生成(例: `feat/user-auth-impl`)
147
+ 2. タスクを分解・依存関係を解析
148
+ 3. 親ブランチ + タスクブランチをworktreeで作成
149
+ 4. ワーカーで並列実行 (TDD: テスト作成 → 実装 → 検証 → レビュー)
150
+ 5. 親ブランチへのマージ
140
151
 
141
- ### 3. 進捗確認
152
+ ### 3. 進捗確認・管理
142
153
 
143
154
  ```bash
144
155
  # ステータス確認
145
- aad status
156
+ aad status [run_id]
146
157
 
147
158
  # 実行の再開
148
159
  aad resume <run_id>
149
160
 
150
161
  # worktreeのクリーンアップ
151
- aad cleanup
162
+ aad cleanup [run_id] [--force]
163
+
164
+ # バージョン確認・アップデート
165
+ aad update [--check]
152
166
  ```
153
167
 
154
168
  ### 4. Webダッシュボード
@@ -256,6 +270,20 @@ describe("MyModule", () => {
256
270
 
257
271
  設定では短縮名(`sonnet`、`opus`、`haiku`)またはフルネームが使用できます。
258
272
 
273
+ ## ブランチフロー
274
+
275
+ AADは要件ドキュメントからconventional commit prefix付きのfeature名を自動生成し、以下のブランチ構造を作成します:
276
+
277
+ ```
278
+ main (ソースブランチ)
279
+ └─ feat/user-auth-impl/parent ← 親ブランチ + worktree
280
+ ├─ feat/user-auth-impl/task-001 ← タスクブランチ(parentから分岐)
281
+ ├─ feat/user-auth-impl/task-002
282
+ └─ feat/user-auth-impl/task-003
283
+ ```
284
+
285
+ 各タスクはparentブランチから分岐し、完了後にparentへマージされます。prefix(`feat/`, `fix/`, `refactor/`等)は要件の内容から自動判定されます。
286
+
259
287
  ## Claude Provider: SDK vs CLI
260
288
 
261
289
  AADはClaudeとの通信に2つのadapterを提供:
@@ -320,9 +348,7 @@ aad cleanup <run_id> --force
320
348
 
321
349
  ## リンク
322
350
 
323
- - [Architecture Documentation](docs/architecture.md)
324
- - [Case Studies](docs/case-studies/) - Real-world execution examples
325
- - [Rewrite Plan (Internal)](docs/rewrite-plan/README.md)
351
+ - [npm Package](https://www.npmjs.com/package/@ronkovic/aad)
326
352
  - [Project Guidelines](CLAUDE.md)
327
353
  - [Issue Tracker](https://github.com/ronkovic/aad/issues)
328
354
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronkovic/aad",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Autonomous Agent Development Orchestrator - Multi-agent TDD pipeline powered by Claude",
5
5
  "module": "src/main.ts",
6
6
  "type": "module",
@@ -77,16 +77,19 @@ export function createApp(options: AppOptions = {}): App {
77
77
  // 2. EventBus初期化
78
78
  const eventBus = new EventBusImpl();
79
79
 
80
- // 3. Logger初期化
80
+ // 3. LogStore初期化 (loggerより先に必要)
81
+ const logStore = new LogStore();
82
+
83
+ // 4. Logger初期化 (dashboard有効時はlogStore+eventBusに橋渡し)
81
84
  const logger = createLogger({
82
85
  service: "aad-cli",
83
86
  debug: config.debug,
87
+ logStore: config.dashboard.enabled ? logStore : undefined,
88
+ eventBus: config.dashboard.enabled ? eventBus : undefined,
84
89
  });
85
90
  logger.debug({ config, options }, "AAD CLI starting");
86
91
 
87
- // 4. LogStore + SSE Transport初期化
88
- const logStore = new LogStore();
89
- // SSE transport registers EventBus listeners internally; no direct reference needed
92
+ // SSE transport: EventBus経由のlog:entryをLogStoreに書く (手動emitされるログ用)
90
93
  createSSETransport({ logStore, eventBus });
91
94
 
92
95
  // 5. ProviderRegistry初期化
@@ -228,17 +228,25 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
228
228
  // List all worktrees
229
229
  const worktrees = await worktreeManager.listWorktrees();
230
230
  const worktreeBase = `${process.cwd()}/.aad/worktrees`;
231
+ // branch field from `git worktree list --porcelain` is full ref: "refs/heads/feat/..."
232
+ const featurePrefix = featureBranchName?.replace(/\/parent$/, "") ?? "";
231
233
  const runWorktrees = worktrees.filter(
232
- (wt) => wt.path.startsWith(worktreeBase)
233
- && (wt.branch.includes(runId) || (featureBranchName && wt.branch.startsWith(featureBranchName.replace(/\/parent$/, "")))) // Match by runId or feature name
234
- && !wt.path.includes(`parent-${runId}`) // Keep parent worktree (contains merge results)
234
+ (wt) => {
235
+ if (!wt.path.startsWith(worktreeBase)) return false;
236
+ const shortBranch = wt.branch.replace(/^refs\/heads\//, "");
237
+ const matchesRun = shortBranch.includes(runId) || (featurePrefix && shortBranch.startsWith(featurePrefix));
238
+ if (!matchesRun) return false;
239
+ // Keep parent worktree (contains merge results for review)
240
+ if (wt.path.includes(`parent-${runId}`)) return false;
241
+ return true;
242
+ }
235
243
  );
236
244
 
237
- // Remove worktrees for this run
245
+ // Remove task worktrees (parent worktree is preserved)
238
246
  let removed = 0;
239
247
  for (const worktree of runWorktrees) {
240
248
  try {
241
- await worktreeManager.removeWorktree(worktree.path, false);
249
+ await worktreeManager.removeWorktree(worktree.path, true);
242
250
  removed++;
243
251
  logger.debug({ path: worktree.path }, "Worktree removed");
244
252
  } catch (error) {
@@ -1,5 +1,7 @@
1
1
  import type { Logger } from "pino";
2
2
  import type { TaskId } from "@aad/shared/types";
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve, join } from "node:path";
3
5
  import { gitExec } from "./git-exec";
4
6
  import { FileLock } from "../persistence";
5
7
 
@@ -14,6 +16,30 @@ export interface MergeResult {
14
16
  message?: string;
15
17
  }
16
18
 
19
+ /**
20
+ * Resolve the actual .git directory for a path.
21
+ * Worktrees have a `.git` file containing "gitdir: <path>",
22
+ * while regular repos have a `.git` directory.
23
+ */
24
+ async function resolveGitDir(worktreePath: string): Promise<string> {
25
+ const dotGit = join(worktreePath, ".git");
26
+ try {
27
+ const stat = await Bun.file(dotGit).exists();
28
+ if (!stat) return dotGit; // fallback
29
+
30
+ const content = await readFile(dotGit, "utf-8");
31
+ const match = content.match(/^gitdir:\s*(.+)$/m);
32
+ if (match?.[1]) {
33
+ const gitdir = match[1].trim();
34
+ // gitdir can be relative or absolute
35
+ return resolve(worktreePath, gitdir);
36
+ }
37
+ return dotGit; // It's a directory, not a file
38
+ } catch {
39
+ return dotGit; // fallback
40
+ }
41
+ }
42
+
17
43
  /**
18
44
  * Merge task branches into parent branch with conflict detection
19
45
  */
@@ -35,24 +61,21 @@ export class MergeService {
35
61
  parentBranch: string,
36
62
  parentWorktree: string
37
63
  ): Promise<MergeResult> {
38
- const lockDir = `${parentWorktree}/.git/aad-merge.lock`;
64
+ const gitDir = await resolveGitDir(parentWorktree);
65
+ const lockDir = join(gitDir, "aad-merge.lock");
39
66
  const lock = new FileLock({ lockDir, timeout: 60000, logger: this.logger });
40
67
 
41
68
  try {
42
69
  await lock.acquire();
43
70
 
44
- this.logger?.info({ taskId, taskBranch, parentBranch }, "Merging task to parent");
45
-
46
- // Fetch latest from task branch
47
- await gitExec(
48
- ["fetch", this.repoRoot, taskBranch],
49
- { cwd: parentWorktree, logger: this.logger }
50
- );
71
+ this.logger?.info({ taskId, taskBranch, parentBranch, parentWorktree }, "Merging task to parent");
51
72
 
52
- // Try merge
73
+ // Worktrees share the same git object store, so we can merge the task
74
+ // branch directly without fetching. This is more reliable than
75
+ // `git fetch <repoRoot> <branch>` + `git merge FETCH_HEAD`.
53
76
  try {
54
77
  await gitExec(
55
- ["merge", "--no-ff", "-m", `Merge task ${taskId as string}: ${taskBranch}`, "FETCH_HEAD"],
78
+ ["merge", "--no-ff", "-m", `Merge task ${taskId as string}: ${taskBranch}`, taskBranch],
56
79
  { cwd: parentWorktree, logger: this.logger }
57
80
  );
58
81
 
@@ -1,13 +1,27 @@
1
1
  import pino from "pino";
2
+ import { Writable } from "node:stream";
3
+ import type { LogStore, LogEntry } from "./log-store";
4
+ import type { EventBus } from "../../shared/events";
2
5
 
3
6
  export interface LoggerOptions {
4
7
  service: string;
5
8
  debug?: boolean;
6
9
  logFilePath?: string;
10
+ /** When provided, all pino logs are also forwarded to LogStore + EventBus for dashboard SSE */
11
+ logStore?: LogStore;
12
+ eventBus?: EventBus;
13
+ }
14
+
15
+ /** Map pino numeric levels to LogEntry level strings */
16
+ function pinoLevelToString(level: number): "debug" | "info" | "warn" | "error" {
17
+ if (level <= 20) return "debug";
18
+ if (level <= 30) return "info";
19
+ if (level <= 40) return "warn";
20
+ return "error";
7
21
  }
8
22
 
9
23
  export function createLogger(options: LoggerOptions): pino.Logger {
10
- const { service, debug = false, logFilePath } = options;
24
+ const { service, debug = false, logFilePath, logStore, eventBus } = options;
11
25
 
12
26
  const targets: pino.TransportTargetOptions[] = [];
13
27
 
@@ -46,10 +60,74 @@ export function createLogger(options: LoggerOptions): pino.Logger {
46
60
  });
47
61
  }
48
62
 
49
- const logger = pino({
63
+ const baseLogger = pino({
50
64
  level: debug ? "debug" : "warn",
51
65
  transport: targets.length === 1 ? targets[0] : { targets },
52
66
  });
53
67
 
54
- return logger.child({ service });
68
+ // If logStore and eventBus provided, create a multistream logger that also
69
+ // writes to the dashboard. We create a separate pino instance that writes
70
+ // to a custom Writable, then hook into the base logger.
71
+ if (logStore && eventBus) {
72
+ const dashboardStream = new Writable({
73
+ write(chunk: Buffer, _encoding: string, callback: () => void): void {
74
+ try {
75
+ const obj = JSON.parse(chunk.toString());
76
+ const entry: LogEntry = {
77
+ level: pinoLevelToString(obj.level ?? 30),
78
+ service: obj.service ?? service,
79
+ message: obj.msg ?? "",
80
+ timestamp: obj.time ?? Date.now(),
81
+ taskId: obj.taskId,
82
+ workerId: obj.workerId,
83
+ };
84
+ logStore.add(entry);
85
+ eventBus.emit({ type: "log:entry", entry });
86
+ } catch {
87
+ // ignore parse errors
88
+ }
89
+ callback();
90
+ },
91
+ });
92
+
93
+ // Use streams for everything when dashboard is active
94
+ // (pino transports run in worker threads, can't combine with custom streams)
95
+ const streams: pino.StreamEntry[] = [];
96
+
97
+ if (debug) {
98
+ // We can't use pino-pretty as a stream directly (it's a transport).
99
+ // Write raw JSON to stderr in debug mode instead.
100
+ streams.push({
101
+ level: "debug" as pino.Level,
102
+ stream: process.stderr,
103
+ });
104
+ } else {
105
+ streams.push({
106
+ level: "warn" as pino.Level,
107
+ stream: process.stderr,
108
+ });
109
+ }
110
+
111
+ if (logFilePath) {
112
+ streams.push({
113
+ level: "info" as pino.Level,
114
+ stream: pino.destination(logFilePath),
115
+ });
116
+ }
117
+
118
+ // Dashboard stream
119
+ streams.push({
120
+ level: (debug ? "debug" : "info") as pino.Level,
121
+ stream: dashboardStream,
122
+ });
123
+
124
+ const combinedLogger = pino(
125
+ { level: debug ? "debug" : "info" },
126
+ pino.multistream(streams)
127
+ );
128
+
129
+ return combinedLogger.child({ service });
130
+ }
131
+
132
+ return baseLogger.child({ service });
55
133
  }