@ronkovic/aad 0.3.5 → 0.3.7

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.
Files changed (26) hide show
  1. package/package.json +4 -3
  2. package/src/__tests__/e2e/pipeline-e2e.test.ts +6 -0
  3. package/src/main.ts +10 -2
  4. package/src/modules/cli/__tests__/interactive/auth-step.test.ts +24 -0
  5. package/src/modules/cli/__tests__/interactive/interactive.test.ts +14 -0
  6. package/src/modules/cli/__tests__/interactive/process-supervisor.test.ts +45 -0
  7. package/src/modules/cli/__tests__/interactive/prompts.test.ts +75 -0
  8. package/src/modules/cli/__tests__/update.test.ts +20 -0
  9. package/src/modules/cli/commands/interactive/auth-step.ts +61 -0
  10. package/src/modules/cli/commands/interactive/launch-step.ts +76 -0
  11. package/src/modules/cli/commands/interactive/process-supervisor.ts +105 -0
  12. package/src/modules/cli/commands/interactive/project-step.ts +157 -0
  13. package/src/modules/cli/commands/interactive/prompts.ts +75 -0
  14. package/src/modules/cli/commands/interactive.ts +38 -0
  15. package/src/modules/cli/commands/resume.ts +16 -0
  16. package/src/modules/cli/commands/run.ts +28 -7
  17. package/src/modules/cli/commands/task-dispatch-handler.ts +11 -3
  18. package/src/modules/cli/commands/update.ts +119 -0
  19. package/src/modules/cli/index.ts +1 -0
  20. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +9 -5
  21. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +19 -4
  22. package/src/modules/git-workspace/branch-manager.ts +10 -3
  23. package/src/modules/git-workspace/worktree-manager.ts +43 -27
  24. package/src/modules/planning/__tests__/feature-name-generator.test.ts +124 -0
  25. package/src/modules/planning/feature-name-generator.ts +103 -0
  26. package/src/modules/task-queue/dispatcher.ts +6 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronkovic/aad",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Autonomous Agent Development Orchestrator - Multi-agent TDD pipeline powered by Claude",
5
5
  "module": "src/main.ts",
6
6
  "type": "module",
@@ -38,11 +38,12 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@anthropic-ai/claude-agent-sdk": "^0.2.34",
41
+ "@inquirer/prompts": "^8.2.0",
42
+ "chokidar": "^4.0",
43
+ "commander": "^13.0",
41
44
  "hono": "^4.6",
42
45
  "pino": "^9.6",
43
46
  "pino-pretty": "^13.0",
44
- "chokidar": "^4.0",
45
- "commander": "^13.0",
46
47
  "zod": "^3.24"
47
48
  },
48
49
  "devDependencies": {
@@ -270,6 +270,12 @@ describe("E2E Pipeline", () => {
270
270
  result: { taskId: createTaskId("task-1"), status: "completed", duration: 50 },
271
271
  });
272
272
 
273
+ // Emit worker:idle to trigger dispatch (mirrors real task-dispatch-handler behavior)
274
+ eventBus.emit({
275
+ type: "worker:idle",
276
+ workerId: createWorkerId("w-1"),
277
+ });
278
+
273
279
  await waitFor(() => dispatchedTasks.length >= 2, 3000);
274
280
 
275
281
  expect(dispatchedTasks[1]).toBe(createTaskId("task-2"));
package/src/main.ts CHANGED
@@ -11,9 +11,11 @@ import {
11
11
  createResumeCommand,
12
12
  createStatusCommand,
13
13
  createCleanupCommand,
14
+ createUpdateCommand,
14
15
  type App,
15
16
  type AppOptions,
16
17
  } from "./modules/cli";
18
+ import { registerInteractiveCommand } from "./modules/cli/commands/interactive.js";
17
19
  import packageJson from "../package.json" with { type: "json" };
18
20
 
19
21
  const program = new Command()
@@ -47,6 +49,12 @@ program.addCommand(createRunCommand(getApp));
47
49
  program.addCommand(createResumeCommand(getApp));
48
50
  program.addCommand(createStatusCommand(getApp));
49
51
  program.addCommand(createCleanupCommand(getApp));
52
+ program.addCommand(createUpdateCommand());
53
+ registerInteractiveCommand(program);
50
54
 
51
- // Parse CLI args
52
- program.parse();
55
+ // Default to interactive mode when no command is given
56
+ if (process.argv.length <= 2) {
57
+ program.parse(["node", "aad", "interactive"]);
58
+ } else {
59
+ program.parse();
60
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { runAuthStep } from "../../commands/interactive/auth-step";
3
+
4
+ // Mock @inquirer/prompts by testing the env detection logic directly
5
+ describe("Auth Step", () => {
6
+ let origApiKey: string | undefined;
7
+ let origOauth: string | undefined;
8
+
9
+ beforeEach(() => {
10
+ origApiKey = process.env.ANTHROPIC_API_KEY;
11
+ origOauth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
12
+ });
13
+
14
+ afterEach(() => {
15
+ if (origApiKey !== undefined) process.env.ANTHROPIC_API_KEY = origApiKey;
16
+ else delete process.env.ANTHROPIC_API_KEY;
17
+ if (origOauth !== undefined) process.env.CLAUDE_CODE_OAUTH_TOKEN = origOauth;
18
+ else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
19
+ });
20
+
21
+ it("module exports runAuthStep function", () => {
22
+ expect(typeof runAuthStep).toBe("function");
23
+ });
24
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { registerInteractiveCommand } from "../../commands/interactive";
3
+ import { Command } from "commander";
4
+
5
+ describe("Interactive Command Registration", () => {
6
+ it("registers interactive command on program", () => {
7
+ const program = new Command();
8
+ registerInteractiveCommand(program);
9
+
10
+ const cmd = program.commands.find((c) => c.name() === "interactive");
11
+ expect(cmd).toBeDefined();
12
+ expect(cmd?.description()).toContain("Interactive");
13
+ });
14
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { spawnProject, killAll } from "../../commands/interactive/process-supervisor";
3
+ import type { ProjectConfig } from "../../commands/interactive/project-step";
4
+ import { mkdtempSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { resolve } from "node:path";
7
+
8
+ describe("Process Supervisor", () => {
9
+ it("exports spawnProject and killAll functions", () => {
10
+ expect(typeof spawnProject).toBe("function");
11
+ expect(typeof killAll).toBe("function");
12
+ });
13
+
14
+ it("spawns a simple process and returns ManagedProcess", async () => {
15
+ const tmpDir = mkdtempSync(resolve(tmpdir(), "aad-test-"));
16
+ const project: ProjectConfig = {
17
+ directory: tmpDir,
18
+ branch: "main",
19
+ requirementsPath: "/dev/null",
20
+ workers: 1,
21
+ };
22
+
23
+ const mp = spawnProject(project, 0);
24
+ expect(mp.dashboardPort).toBe(7333);
25
+ expect(mp.project).toBe(project);
26
+ expect(mp.proc).toBeDefined();
27
+
28
+ // Kill immediately
29
+ await killAll([mp]);
30
+ });
31
+
32
+ it("increments dashboard port by index", async () => {
33
+ const tmpDir = mkdtempSync(resolve(tmpdir(), "aad-test-"));
34
+ const project: ProjectConfig = {
35
+ directory: tmpDir,
36
+ branch: "main",
37
+ requirementsPath: "/dev/null",
38
+ workers: 1,
39
+ };
40
+
41
+ const mp = spawnProject(project, 3);
42
+ expect(mp.dashboardPort).toBe(7336);
43
+ await killAll([mp]);
44
+ });
45
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { printBox, success, warn, error } from "../../commands/interactive/prompts";
3
+
4
+ describe("Interactive Prompts Utilities", () => {
5
+ describe("printBox", () => {
6
+ it("prints a box with lines", () => {
7
+ const logs: string[] = [];
8
+ const origLog = console.log;
9
+ console.log = (...args: unknown[]) => logs.push(args.join(" "));
10
+ try {
11
+ printBox(["line 1", "line 2"]);
12
+ expect(logs.length).toBe(4); // top + 2 lines + bottom
13
+ expect(logs[0]).toContain("┌");
14
+ expect(logs[3]).toContain("└");
15
+ } finally {
16
+ console.log = origLog;
17
+ }
18
+ });
19
+
20
+ it("prints a box with title", () => {
21
+ const logs: string[] = [];
22
+ const origLog = console.log;
23
+ console.log = (...args: unknown[]) => logs.push(args.join(" "));
24
+ try {
25
+ printBox(["content"], "Title");
26
+ expect(logs.length).toBe(5); // top + title + separator + content + bottom
27
+ expect(logs[1]).toContain("Title");
28
+ expect(logs[2]).toContain("├");
29
+ } finally {
30
+ console.log = origLog;
31
+ }
32
+ });
33
+ });
34
+
35
+ describe("status helpers", () => {
36
+ it("success prints green check", () => {
37
+ const logs: string[] = [];
38
+ const origLog = console.log;
39
+ console.log = (...args: unknown[]) => logs.push(args.join(" "));
40
+ try {
41
+ success("done");
42
+ expect(logs[0]).toContain("✓");
43
+ expect(logs[0]).toContain("done");
44
+ } finally {
45
+ console.log = origLog;
46
+ }
47
+ });
48
+
49
+ it("warn prints yellow warning", () => {
50
+ const logs: string[] = [];
51
+ const origLog = console.log;
52
+ console.log = (...args: unknown[]) => logs.push(args.join(" "));
53
+ try {
54
+ warn("caution");
55
+ expect(logs[0]).toContain("⚠");
56
+ expect(logs[0]).toContain("caution");
57
+ } finally {
58
+ console.log = origLog;
59
+ }
60
+ });
61
+
62
+ it("error prints red cross", () => {
63
+ const logs: string[] = [];
64
+ const origLog = console.log;
65
+ console.log = (...args: unknown[]) => logs.push(args.join(" "));
66
+ try {
67
+ error("fail");
68
+ expect(logs[0]).toContain("✗");
69
+ expect(logs[0]).toContain("fail");
70
+ } finally {
71
+ console.log = origLog;
72
+ }
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { createUpdateCommand } from "../commands/update";
3
+
4
+ describe("Update Command", () => {
5
+ it("creates a command named 'update'", () => {
6
+ const cmd = createUpdateCommand();
7
+ expect(cmd.name()).toBe("update");
8
+ });
9
+
10
+ it("has --check option", () => {
11
+ const cmd = createUpdateCommand();
12
+ const checkOpt = cmd.options.find((o) => o.long === "--check");
13
+ expect(checkOpt).toBeDefined();
14
+ });
15
+
16
+ it("has description", () => {
17
+ const cmd = createUpdateCommand();
18
+ expect(cmd.description()).toContain("Update");
19
+ });
20
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Auth check / input step for interactive mode
3
+ */
4
+ import { askConfirm, askSelect, askPassword, success, warn } from "./prompts.js";
5
+
6
+ export interface AuthResult {
7
+ method: "api-key" | "oauth";
8
+ token: string;
9
+ }
10
+
11
+ function maskToken(token: string): string {
12
+ if (token.length <= 8) return "••••••••";
13
+ return `${token.slice(0, 6)}...${token.slice(-4)}`;
14
+ }
15
+
16
+ export async function runAuthStep(): Promise<AuthResult> {
17
+ const apiKey = process.env.ANTHROPIC_API_KEY;
18
+ const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
19
+
20
+ // Existing API key detected
21
+ if (apiKey) {
22
+ console.log(`\n API Key検出: ${maskToken(apiKey)}`);
23
+ const useExisting = await askConfirm("この設定を使用しますか?", true);
24
+ if (useExisting) {
25
+ success("API Key認証を使用");
26
+ return { method: "api-key", token: apiKey };
27
+ }
28
+ }
29
+
30
+ // Existing OAuth token detected
31
+ if (oauthToken) {
32
+ console.log(`\n OAuth Token検出: ${maskToken(oauthToken)}`);
33
+ const useExisting = await askConfirm("この設定を使用しますか?", true);
34
+ if (useExisting) {
35
+ success("OAuth認証を使用");
36
+ return { method: "oauth", token: oauthToken };
37
+ }
38
+ }
39
+
40
+ // Manual input
41
+ if (!apiKey && !oauthToken) {
42
+ warn("認証トークンが未設定です");
43
+ }
44
+
45
+ const method = await askSelect<"api-key" | "oauth">("認証方式を選択:", [
46
+ { name: "API Key (ANTHROPIC_API_KEY)", value: "api-key" },
47
+ { name: "OAuth Token (CLAUDE_CODE_OAUTH_TOKEN)", value: "oauth" },
48
+ ]);
49
+
50
+ const token = await askPassword("トークンを入力:");
51
+
52
+ // Set in process env for this session
53
+ if (method === "api-key") {
54
+ process.env.ANTHROPIC_API_KEY = token;
55
+ } else {
56
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
57
+ }
58
+
59
+ success(`${method === "api-key" ? "API Key" : "OAuth"}認証を設定(セッション限定)`);
60
+ return { method, token };
61
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Launch confirmation + execution step
3
+ */
4
+ import { basename } from "node:path";
5
+ import { getMemoryStatus } from "@aad/shared/memory-check";
6
+ import type { ProjectConfig } from "./project-step.js";
7
+ import {
8
+ type ManagedProcess,
9
+ spawnAll,
10
+ setupSignalHandler,
11
+ } from "./process-supervisor.js";
12
+ import { askConfirm, printBox, success } from "./prompts.js";
13
+
14
+ function formatPlanLines(projects: ProjectConfig[]): string[] {
15
+ const lines: string[] = [];
16
+ for (let i = 0; i < projects.length; i++) {
17
+ const p = projects[i]!;
18
+ lines.push(`#${i + 1} ${p.directory}`);
19
+ lines.push(` ブランチ: ${p.branch}`);
20
+ lines.push(` 要件: ${p.requirementsPath}`);
21
+ lines.push(` ワーカー: ${p.workers}`);
22
+ if (i < projects.length - 1) lines.push("");
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ export async function runLaunchStep(
28
+ projects: ProjectConfig[],
29
+ ): Promise<ManagedProcess[] | null> {
30
+ const memStatus = await getMemoryStatus();
31
+ const availableGB = memStatus.freeGB;
32
+ const totalWorkers = projects.reduce((sum, p) => sum + p.workers, 0);
33
+
34
+ const planLines = formatPlanLines(projects);
35
+ planLines.push("");
36
+ planLines.push(
37
+ `合計ワーカー: ${totalWorkers} メモリ: ${availableGB.toFixed(1)}GB free`,
38
+ );
39
+
40
+ printBox(planLines, "実行プラン");
41
+ console.log();
42
+
43
+ const confirmed = await askConfirm("実行しますか?", true);
44
+ if (!confirmed) {
45
+ console.log("\n 実行をキャンセルしました\n");
46
+ return null;
47
+ }
48
+
49
+ console.log();
50
+ const managed = spawnAll(projects);
51
+
52
+ for (const mp of managed) {
53
+ const name = basename(mp.project.directory);
54
+ success(`${name} 起動 — Dashboard: http://localhost:${mp.dashboardPort}`);
55
+ }
56
+
57
+ setupSignalHandler(managed);
58
+
59
+ console.log("\n 全プロジェクト起動完了。ダッシュボードで進捗を確認できます。");
60
+ console.log(" 終了: Ctrl+C (全プロジェクト停止)\n");
61
+
62
+ // Wait for all processes to complete
63
+ const exitCodes = await Promise.all(managed.map((mp) => mp.proc.exited));
64
+ for (let i = 0; i < exitCodes.length; i++) {
65
+ const mp = managed[i]!;
66
+ const code = exitCodes[i]!;
67
+ const name = basename(mp.project.directory);
68
+ if (code === 0) {
69
+ success(`${name} 完了 (exit: 0)`);
70
+ } else {
71
+ console.log(` \x1b[31m✗\x1b[0m ${name} 終了 (exit: ${code})`);
72
+ }
73
+ }
74
+
75
+ return managed;
76
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Child process management — spawn, monitor, and terminate AAD runs
3
+ */
4
+ import { resolve } from "node:path";
5
+ import { mkdirSync, existsSync, openSync } from "node:fs";
6
+ import type { ProjectConfig } from "./project-step.js";
7
+
8
+ const BASE_DASHBOARD_PORT = 7333;
9
+
10
+ export interface ManagedProcess {
11
+ project: ProjectConfig;
12
+ proc: ReturnType<typeof Bun.spawn>;
13
+ dashboardPort: number;
14
+ logDir: string;
15
+ }
16
+
17
+ function ensureLogDir(directory: string, runId: string): string {
18
+ const logDir = resolve(directory, ".aad", "logs", runId);
19
+ if (!existsSync(logDir)) {
20
+ mkdirSync(logDir, { recursive: true });
21
+ }
22
+ return logDir;
23
+ }
24
+
25
+ function generateRunId(): string {
26
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
27
+ }
28
+
29
+ export function spawnProject(
30
+ project: ProjectConfig,
31
+ index: number,
32
+ ): ManagedProcess {
33
+ const runId = generateRunId();
34
+ const logDir = ensureLogDir(project.directory, runId);
35
+ const dashboardPort = BASE_DASHBOARD_PORT + index;
36
+
37
+ const stdoutFd = openSync(resolve(logDir, "stdout.log"), "w");
38
+ const stderrFd = openSync(resolve(logDir, "stderr.log"), "w");
39
+
40
+ const proc = Bun.spawn(
41
+ [
42
+ "bun",
43
+ "run",
44
+ resolve(import.meta.dir, "../../../../main.ts"),
45
+ "run",
46
+ project.requirementsPath,
47
+ "--branch",
48
+ project.branch,
49
+ "--workers",
50
+ String(project.workers),
51
+ ],
52
+ {
53
+ cwd: project.directory,
54
+ stdout: stdoutFd,
55
+ stderr: stderrFd,
56
+ env: {
57
+ ...process.env,
58
+ AAD_DASHBOARD_PORT: String(dashboardPort),
59
+ },
60
+ },
61
+ );
62
+
63
+ return { project, proc, dashboardPort, logDir };
64
+ }
65
+
66
+ export function spawnAll(projects: ProjectConfig[]): ManagedProcess[] {
67
+ return projects.map((project, i) => spawnProject(project, i));
68
+ }
69
+
70
+ export async function killAll(processes: ManagedProcess[]): Promise<void> {
71
+ for (const mp of processes) {
72
+ try {
73
+ mp.proc.kill("SIGTERM");
74
+ } catch {
75
+ // Process may have already exited
76
+ }
77
+ }
78
+
79
+ // Wait for all to exit (with timeout)
80
+ const timeout = setTimeout(() => {
81
+ for (const mp of processes) {
82
+ try {
83
+ mp.proc.kill("SIGKILL");
84
+ } catch {
85
+ // ignore
86
+ }
87
+ }
88
+ }, 5000);
89
+
90
+ await Promise.allSettled(processes.map((mp) => mp.proc.exited));
91
+ clearTimeout(timeout);
92
+ }
93
+
94
+ export function setupSignalHandler(processes: ManagedProcess[]): void {
95
+ const handler = (): void => {
96
+ console.log("\n\n 全プロセスを停止中...");
97
+ void killAll(processes).then(() => {
98
+ console.log(" 停止完了\n");
99
+ process.exit(0);
100
+ });
101
+ };
102
+
103
+ process.on("SIGINT", handler);
104
+ process.on("SIGTERM", handler);
105
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Project configuration step — repeatable loop
3
+ */
4
+ import { existsSync, statSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { gitExec } from "@aad/git-workspace/git-exec";
7
+ import { getMemoryStatus } from "@aad/shared/memory-check";
8
+ import {
9
+ askConfirm,
10
+ askInput,
11
+ askSelect,
12
+ success,
13
+ warn,
14
+ error,
15
+ } from "./prompts.js";
16
+
17
+ export interface ProjectConfig {
18
+ directory: string;
19
+ branch: string;
20
+ requirementsPath: string;
21
+ workers: number;
22
+ }
23
+
24
+ async function isGitRepo(dir: string): Promise<boolean> {
25
+ try {
26
+ await gitExec(["rev-parse", "--is-inside-work-tree"], { cwd: dir });
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ async function listBranches(dir: string): Promise<string[]> {
34
+ const result = await gitExec(["branch", "--format=%(refname:short)"], {
35
+ cwd: dir,
36
+ });
37
+ return result.stdout
38
+ .split("\n")
39
+ .map((b: string) => b.trim())
40
+ .filter(Boolean);
41
+ }
42
+
43
+ async function getCurrentBranch(dir: string): Promise<string> {
44
+ const result = await gitExec(["branch", "--show-current"], { cwd: dir });
45
+ return result.stdout.trim();
46
+ }
47
+
48
+ async function pullBranch(dir: string, branch: string): Promise<boolean> {
49
+ try {
50
+ await gitExec(["checkout", branch], { cwd: dir });
51
+ await gitExec(["pull", "--ff-only"], { cwd: dir });
52
+ success(`${branch} を最新に更新`);
53
+ return true;
54
+ } catch (e) {
55
+ warn(`git pull失敗: ${e instanceof Error ? e.message : String(e)}`);
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function suggestWorkers(availableGB: number): number {
61
+ if (availableGB >= 12) return 3;
62
+ if (availableGB >= 8) return 2;
63
+ return 1;
64
+ }
65
+
66
+ async function configureProject(index: number): Promise<ProjectConfig> {
67
+ console.log(`\n── プロジェクト #${index + 1} ──\n`);
68
+
69
+ // Directory
70
+ const directory = await askInput("プロジェクトディレクトリパス:", {
71
+ validate: (v) => {
72
+ const resolved = resolve(v);
73
+ if (!existsSync(resolved)) return "ディレクトリが存在しません";
74
+ if (!statSync(resolved).isDirectory()) return "ディレクトリではありません";
75
+ return true;
76
+ },
77
+ });
78
+
79
+ const resolvedDir = resolve(directory);
80
+
81
+ if (!(await isGitRepo(resolvedDir))) {
82
+ error("gitリポジトリが検出されませんでした");
83
+ throw new Error(`Not a git repository: ${resolvedDir}`);
84
+ }
85
+ success("gitリポジトリ検出");
86
+
87
+ // Branch selection
88
+ const branches = await listBranches(resolvedDir);
89
+ const currentBranch = await getCurrentBranch(resolvedDir);
90
+ const branchChoices = branches.map((b) => ({
91
+ name: b === currentBranch ? `${b} (現在)` : b,
92
+ value: b,
93
+ }));
94
+
95
+ const branch = await askSelect("worktree元ブランチ:", branchChoices);
96
+
97
+ // Pull
98
+ const doPull = await askConfirm("ブランチを最新にしますか? (git pull)", true);
99
+ if (doPull) {
100
+ await pullBranch(resolvedDir, branch);
101
+ }
102
+
103
+ // Requirements file
104
+ const requirementsPath = await askInput("要件定義ドキュメントパス:", {
105
+ validate: (v) => {
106
+ const absPath = resolve(resolvedDir, v);
107
+ if (!existsSync(absPath)) return `ファイルが存在しません: ${absPath}`;
108
+ return true;
109
+ },
110
+ });
111
+ success("ファイル存在確認");
112
+
113
+ // Workers
114
+ const memStatus = await getMemoryStatus();
115
+ const availableGB = memStatus.freeGB;
116
+ const suggested = suggestWorkers(availableGB);
117
+
118
+ const workersStr = await askInput(
119
+ `並行ワーカー数 (推奨: ${suggested}, 空きメモリ: ${availableGB.toFixed(1)}GB):`,
120
+ {
121
+ default: String(suggested),
122
+ validate: (v) => {
123
+ const n = parseInt(v, 10);
124
+ if (isNaN(n) || n < 1) return "1以上の整数を入力してください";
125
+ if (n > 8) return "最大8ワーカーまでです";
126
+ return true;
127
+ },
128
+ },
129
+ );
130
+
131
+ const workers = parseInt(workersStr, 10);
132
+
133
+ console.log(`\n────────────────────────────`);
134
+ console.log(` プロジェクト #${index + 1} 設定完了`);
135
+ console.log(`────────────────────────────`);
136
+
137
+ return {
138
+ directory: resolvedDir,
139
+ branch,
140
+ requirementsPath: resolve(resolvedDir, requirementsPath),
141
+ workers,
142
+ };
143
+ }
144
+
145
+ export async function runProjectStep(): Promise<ProjectConfig[]> {
146
+ const projects: ProjectConfig[] = [];
147
+
148
+ // At least one project
149
+ projects.push(await configureProject(0));
150
+
151
+ // Additional projects
152
+ while (await askConfirm("別のプロジェクトを追加しますか?", false)) {
153
+ projects.push(await configureProject(projects.length));
154
+ }
155
+
156
+ return projects;
157
+ }