@ronkovic/aad 0.3.7 → 0.3.8
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)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://bun.sh/)
|
|
8
|
-
[](https://www.npmjs.com/package/@ronkovic/aad)
|
|
9
|
+
[]()
|
|
10
|
+
[]()
|
|
10
11
|
|
|
11
12
|
## 主要機能
|
|
12
13
|
|
|
13
14
|
- **マルチエージェント並列実行**: 複数のClaudeエージェントが独立したタスクを並列処理
|
|
14
15
|
- **TDDパイプライン**: Red → Green → Verify → Review → Merge の自動化されたテスト駆動開発
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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.
|
|
138
|
-
3.
|
|
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
|
-
- [
|
|
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
package/src/modules/cli/app.ts
CHANGED
|
@@ -77,16 +77,19 @@ export function createApp(options: AppOptions = {}): App {
|
|
|
77
77
|
// 2. EventBus初期化
|
|
78
78
|
const eventBus = new EventBusImpl();
|
|
79
79
|
|
|
80
|
-
// 3.
|
|
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
|
-
//
|
|
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) =>
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
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,
|
|
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,7 +61,8 @@ export class MergeService {
|
|
|
35
61
|
parentBranch: string,
|
|
36
62
|
parentWorktree: string
|
|
37
63
|
): Promise<MergeResult> {
|
|
38
|
-
const
|
|
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 {
|
|
@@ -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
|
|
63
|
+
const baseLogger = pino({
|
|
50
64
|
level: debug ? "debug" : "warn",
|
|
51
65
|
transport: targets.length === 1 ? targets[0] : { targets },
|
|
52
66
|
});
|
|
53
67
|
|
|
54
|
-
|
|
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
|
}
|