@ronkovic/aad 0.3.9 → 0.4.0

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 (114) hide show
  1. package/README.md +292 -12
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
  4. package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
  5. package/src/__tests__/integration/pipeline.test.ts +1 -0
  6. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  7. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
  8. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
  9. package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
  10. package/src/modules/cli/__tests__/resume.test.ts +1 -0
  11. package/src/modules/cli/__tests__/run.test.ts +1 -0
  12. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  13. package/src/modules/cli/commands/cleanup.ts +26 -11
  14. package/src/modules/cli/commands/resume.ts +3 -2
  15. package/src/modules/cli/commands/run.ts +57 -7
  16. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  17. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  18. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  19. package/src/modules/dashboard/routes/sse.ts +3 -2
  20. package/src/modules/dashboard/server.ts +1 -0
  21. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  22. package/src/modules/dashboard/ui/dashboard.html +143 -18
  23. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  24. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  25. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  26. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  27. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  28. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  29. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  30. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  31. package/src/modules/git-workspace/branch-manager.ts +24 -3
  32. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  33. package/src/modules/git-workspace/git-exec.ts +3 -2
  34. package/src/modules/git-workspace/index.ts +10 -1
  35. package/src/modules/git-workspace/merge-service.ts +36 -2
  36. package/src/modules/git-workspace/pr-manager.ts +278 -0
  37. package/src/modules/git-workspace/template-copy.ts +302 -0
  38. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  39. package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
  40. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  41. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  42. package/src/modules/planning/planning.service.ts +16 -2
  43. package/src/modules/planning/project-detection.ts +4 -1
  44. package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
  45. package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
  46. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  47. package/src/modules/task-execution/executor.ts +87 -4
  48. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  49. package/src/modules/task-execution/phases/merge.ts +44 -2
  50. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  51. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  52. package/src/modules/task-queue/dispatcher.ts +50 -1
  53. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  54. package/src/shared/config.ts +6 -0
  55. package/src/shared/prerequisites.ts +190 -0
  56. package/src/shared/types.ts +13 -0
  57. package/templates/CLAUDE.md +122 -0
  58. package/templates/settings.json +117 -0
  59. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  60. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  61. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  62. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  63. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  64. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  65. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  66. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  67. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  68. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  69. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  70. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  71. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  72. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  73. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  74. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  75. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  76. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -1,5 +1,6 @@
1
1
  import type { Task, WorkspaceInfo, EffortLevel, PhaseResult } from "@aad/shared/types";
2
2
  import type { ClaudeProvider } from "@aad/claude-provider";
3
+ import type { RetryContext } from "../executor";
3
4
 
4
5
  export interface TesterRedOptions {
5
6
  effortLevel?: EffortLevel;
@@ -10,10 +11,10 @@ export interface TesterRedOptions {
10
11
  /**
11
12
  * Build TDD Red phase prompt for tester agent
12
13
  */
13
- export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo): string {
14
+ export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo, retryContext?: RetryContext): string {
14
15
  const languagePatterns = getLanguageTestPatterns(workspace.language);
15
16
 
16
- return `testerエージェントとして、TDD Red フェーズを実行してください。
17
+ let prompt = `testerエージェントとして、TDD Red フェーズを実行してください。
17
18
 
18
19
  Task ID: ${task.taskId as string}
19
20
  Task Title: ${task.title}
@@ -28,11 +29,26 @@ Task Description: ${task.description}
28
29
 
29
30
  実行内容:
30
31
  1. タスクの要件を理解する
31
- 2. 失敗するテストを作成する(言語に応じた適切なパターンを使用)
32
+ 2. 失敗するテストを作成する(言語に応じた適切なパターンを使用)
32
33
  ${languagePatterns}
33
34
  3. テストを実行して失敗することを確認する
34
35
 
35
36
  注意: このフェーズでは実装コードは書かないでください。テストのみ作成してください。`;
37
+
38
+ if (retryContext?.previousFailure) {
39
+ prompt += `\n\n⚠️ リトライ情報 (${retryContext.retryCount}回目):
40
+ 前回のフェーズ「${retryContext.previousFailure.phase}」で失敗しました。
41
+ エラー: ${retryContext.previousFailure.error}`;
42
+
43
+ if (retryContext.previousFailure.testOutput) {
44
+ prompt += `\n\n前回のテスト出力:
45
+ ${retryContext.previousFailure.testOutput}`;
46
+ }
47
+
48
+ prompt += `\n\n前回の失敗を踏まえて、より堅牢なテストを作成してください。`;
49
+ }
50
+
51
+ return prompt;
36
52
  }
37
53
 
38
54
  /**
@@ -72,9 +88,10 @@ export async function runTesterRed(
72
88
  task: Task,
73
89
  workspace: WorkspaceInfo,
74
90
  provider: ClaudeProvider,
75
- options: TesterRedOptions = {}
91
+ options: TesterRedOptions = {},
92
+ retryContext?: RetryContext
76
93
  ): Promise<PhaseResult> {
77
- const prompt = buildRedPhasePrompt(task, workspace);
94
+ const prompt = buildRedPhasePrompt(task, workspace, retryContext);
78
95
 
79
96
  const response = await provider.call({
80
97
  prompt,
@@ -48,8 +48,12 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
48
48
  if (packageManager === "yarn") return ["yarn", "test"];
49
49
  return ["npx", "mocha"];
50
50
 
51
- case "pytest":
51
+ case "pytest": {
52
+ const { packageManager } = workspace;
53
+ if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
54
+ if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
52
55
  return ["pytest", "-v"];
56
+ }
53
57
 
54
58
  case "go-test":
55
59
  return ["go", "test", "./..."];
@@ -63,11 +67,23 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
63
67
  case "gradle":
64
68
  return ["./gradlew", "test"];
65
69
 
66
- case "unknown":
67
- throw new TestRunnerError(
68
- "Unsupported test framework: unknown",
69
- { testFramework }
70
- );
70
+ case "playwright":
71
+ return ["npx", "playwright", "test"];
72
+
73
+ case "terraform":
74
+ return ["terraform", "validate"];
75
+
76
+ case "unknown": {
77
+ // Fallback: use package manager-based test command
78
+ const { packageManager } = workspace;
79
+ if (packageManager === "bun") return ["bun", "test"];
80
+ if (packageManager === "npm") return ["npm", "test"];
81
+ if (packageManager === "yarn") return ["yarn", "test"];
82
+ if (packageManager === "pnpm") return ["pnpm", "test"];
83
+ if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
84
+ if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
85
+ return ["npm", "test"];
86
+ }
71
87
 
72
88
  default: {
73
89
  const exhaustive: never = testFramework;
@@ -248,6 +248,13 @@ export class Dispatcher {
248
248
  task.retryCount += 1;
249
249
  task.workerId = undefined;
250
250
  task.failureReason = error;
251
+ // Store structured failure context for retry
252
+ task.previousFailure = {
253
+ phase: this.extractPhaseFromError(error),
254
+ error: error,
255
+ testOutput: this.extractTestOutput(error),
256
+ retryCount: task.retryCount,
257
+ };
251
258
  await this.deps.taskStore.save(task);
252
259
  this.taskMap.set(taskId as string, task);
253
260
 
@@ -369,10 +376,52 @@ export class Dispatcher {
369
376
 
370
377
  this.deps.logger.warn(
371
378
  { taskId: task.taskId, elapsed },
372
- "Stale task detected"
379
+ "Stale task detected, triggering retry"
380
+ );
381
+
382
+ // Trigger retry flow via handleTaskFailed
383
+ void this.handleTaskFailed(
384
+ task.taskId,
385
+ `Task stale (elapsed: ${elapsed}ms)`
373
386
  );
374
387
  }
375
388
  }
376
389
  }
377
390
  }
391
+
392
+ /**
393
+ * Extract phase name from error message
394
+ */
395
+ private extractPhaseFromError(error: string): string {
396
+ const lowerError = error.toLowerCase();
397
+ if (lowerError.includes("red phase")) return "red";
398
+ if (lowerError.includes("green phase")) return "green";
399
+ if (lowerError.includes("verify phase")) return "verify";
400
+ if (lowerError.includes("review phase")) return "review";
401
+ if (lowerError.includes("merge phase")) return "merge";
402
+ return "unknown";
403
+ }
404
+
405
+ /**
406
+ * Extract test output from error context (truncated to 2000 chars)
407
+ */
408
+ private extractTestOutput(error: string): string | undefined {
409
+ try {
410
+ // Try to parse JSON context from PhaseError
411
+ const jsonMatch = error.match(/\{.*\}/s);
412
+ if (!jsonMatch) return undefined;
413
+
414
+ const context = JSON.parse(jsonMatch[0]);
415
+ const output = context.output ?? context.error;
416
+ if (!output) return undefined;
417
+
418
+ // Truncate to 2000 chars
419
+ return output.length > 2000
420
+ ? output.substring(0, 2000) + "\n... (truncated)"
421
+ : output;
422
+ } catch {
423
+ // If parsing fails, return undefined
424
+ return undefined;
425
+ }
426
+ }
378
427
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Prerequisites Tests
3
+ * Verifies prerequisite checks for AAD pipeline
4
+ */
5
+
6
+ import { describe, test, expect, beforeEach } from "bun:test";
7
+ import { checkPrerequisites } from "../prerequisites";
8
+ import { pino } from "pino";
9
+
10
+ describe("checkPrerequisites", () => {
11
+ let logger: import("pino").Logger;
12
+
13
+ beforeEach(() => {
14
+ logger = pino({ level: "silent" });
15
+ });
16
+
17
+ test("runs all prerequisite checks", async () => {
18
+ const result = await checkPrerequisites(logger);
19
+
20
+ expect(result).toBeDefined();
21
+ expect(result.passed).toBeDefined();
22
+ expect(Array.isArray(result.failures)).toBe(true);
23
+ expect(Array.isArray(result.warnings)).toBe(true);
24
+ });
25
+
26
+ test("passes if all required checks pass with ANTHROPIC_API_KEY", async () => {
27
+ // In a git repo with ANTHROPIC_API_KEY set, all required checks should pass
28
+ // This test assumes we're running in a valid git repo
29
+ const originalKey = process.env.ANTHROPIC_API_KEY;
30
+ const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
31
+ process.env.ANTHROPIC_API_KEY = "test-key";
32
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
33
+
34
+ try {
35
+ const result = await checkPrerequisites(logger);
36
+
37
+ // Git, git-version, git-repo, and claude-auth should all pass
38
+ // If we're in a git repo, failures should be 0
39
+ // If not (e.g., running in a worktree without git), we skip this assertion
40
+ if (result.passed) {
41
+ expect(result.failures.length).toBe(0);
42
+ expect(result.passed).toBe(true);
43
+ } else {
44
+ // We're likely in a non-git environment or worktree - just verify structure
45
+ expect(result.failures).toBeDefined();
46
+ expect(result.warnings).toBeDefined();
47
+ }
48
+ } finally {
49
+ if (originalKey) {
50
+ process.env.ANTHROPIC_API_KEY = originalKey;
51
+ } else {
52
+ delete process.env.ANTHROPIC_API_KEY;
53
+ }
54
+ if (originalOAuth) {
55
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
56
+ } else {
57
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
58
+ }
59
+ }
60
+ });
61
+
62
+ test("passes if CLAUDE_CODE_OAUTH_TOKEN is set", async () => {
63
+ const originalKey = process.env.ANTHROPIC_API_KEY;
64
+ const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
65
+ delete process.env.ANTHROPIC_API_KEY;
66
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token";
67
+
68
+ try {
69
+ const result = await checkPrerequisites(logger);
70
+
71
+ // Should pass auth check with OAuth token
72
+ const authFailure = result.failures.find((f) => f.name === "claude-auth");
73
+ expect(authFailure).toBeUndefined();
74
+ } finally {
75
+ if (originalKey) {
76
+ process.env.ANTHROPIC_API_KEY = originalKey;
77
+ } else {
78
+ delete process.env.ANTHROPIC_API_KEY;
79
+ }
80
+ if (originalOAuth) {
81
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
82
+ } else {
83
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
84
+ }
85
+ }
86
+ });
87
+
88
+ test("passes if both ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN are set", async () => {
89
+ const originalKey = process.env.ANTHROPIC_API_KEY;
90
+ const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
91
+ process.env.ANTHROPIC_API_KEY = "test-key";
92
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token";
93
+
94
+ try {
95
+ const result = await checkPrerequisites(logger);
96
+
97
+ // Should pass auth check with both set
98
+ const authFailure = result.failures.find((f) => f.name === "claude-auth");
99
+ expect(authFailure).toBeUndefined();
100
+ } finally {
101
+ if (originalKey) {
102
+ process.env.ANTHROPIC_API_KEY = originalKey;
103
+ } else {
104
+ delete process.env.ANTHROPIC_API_KEY;
105
+ }
106
+ if (originalOAuth) {
107
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
108
+ } else {
109
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
110
+ }
111
+ }
112
+ });
113
+
114
+ test("fails if neither ANTHROPIC_API_KEY nor CLAUDE_CODE_OAUTH_TOKEN is set", async () => {
115
+ const originalKey = process.env.ANTHROPIC_API_KEY;
116
+ const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
117
+ delete process.env.ANTHROPIC_API_KEY;
118
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
119
+
120
+ try {
121
+ const result = await checkPrerequisites(logger);
122
+
123
+ expect(result.passed).toBe(false);
124
+ expect(result.failures.some((f) => f.name === "claude-auth")).toBe(true);
125
+ } finally {
126
+ if (originalKey) {
127
+ process.env.ANTHROPIC_API_KEY = originalKey;
128
+ }
129
+ if (originalOAuth) {
130
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
131
+ }
132
+ }
133
+ });
134
+
135
+ test("includes warnings for non-required checks", async () => {
136
+ const originalKey = process.env.ANTHROPIC_API_KEY;
137
+ process.env.ANTHROPIC_API_KEY = "test-key";
138
+
139
+ try {
140
+ const result = await checkPrerequisites(logger);
141
+
142
+ // Clean working directory is optional, may appear in warnings
143
+ // (depends on current git state)
144
+ expect(result.warnings).toBeDefined();
145
+ } finally {
146
+ if (originalKey) {
147
+ process.env.ANTHROPIC_API_KEY = originalKey;
148
+ } else {
149
+ delete process.env.ANTHROPIC_API_KEY;
150
+ }
151
+ }
152
+ });
153
+
154
+ test("formats error messages properly", async () => {
155
+ const originalKey = process.env.ANTHROPIC_API_KEY;
156
+ const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
157
+ delete process.env.ANTHROPIC_API_KEY;
158
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
159
+
160
+ try {
161
+ const result = await checkPrerequisites(logger);
162
+
163
+ const authFailure = result.failures.find((f) => f.name === "claude-auth");
164
+ expect(authFailure).toBeDefined();
165
+ expect(authFailure?.message).toContain("ANTHROPIC_API_KEY");
166
+ expect(authFailure?.message).toContain("CLAUDE_CODE_OAUTH_TOKEN");
167
+ } finally {
168
+ if (originalKey) {
169
+ process.env.ANTHROPIC_API_KEY = originalKey;
170
+ }
171
+ if (originalOAuth) {
172
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
173
+ }
174
+ }
175
+ });
176
+ });
@@ -38,6 +38,9 @@ const configSchema = z.object({
38
38
  port: z.number().int().min(1).max(65535),
39
39
  host: z.string(),
40
40
  }),
41
+ git: z.object({
42
+ autoPush: z.boolean(),
43
+ }),
41
44
  repos: z.array(z.object({
42
45
  name: z.string().optional(),
43
46
  path: z.string(),
@@ -111,6 +114,9 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
111
114
  port: parseIntOrDefault(env.AAD_DASHBOARD_PORT, 7333),
112
115
  host: env.AAD_DASHBOARD_HOST ?? "localhost",
113
116
  },
117
+ git: {
118
+ autoPush: parseBoolOrDefault(env.AAD_GIT_AUTO_PUSH, true),
119
+ },
114
120
  repos: env.AAD_REPOS
115
121
  ? env.AAD_REPOS.split(",").map((p) => ({ path: p.trim() }))
116
122
  : undefined,
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Prerequisites Checker
3
+ * Verifies required tools and environment before running AAD pipeline
4
+ */
5
+
6
+ import type { Logger } from "pino";
7
+
8
+ export interface PrerequisiteCheck {
9
+ name: string;
10
+ required: boolean;
11
+ check: () => Promise<boolean>;
12
+ errorMessage: string;
13
+ }
14
+
15
+ export interface PrerequisiteResult {
16
+ passed: boolean;
17
+ failures: Array<{ name: string; message: string }>;
18
+ warnings: Array<{ name: string; message: string }>;
19
+ }
20
+
21
+ /**
22
+ * Check if a command exists in PATH
23
+ */
24
+ async function commandExists(command: string): Promise<boolean> {
25
+ try {
26
+ const proc = Bun.spawn(["which", command], {
27
+ stdout: "pipe",
28
+ stderr: "pipe",
29
+ });
30
+ await proc.exited;
31
+ return proc.exitCode === 0;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check git version (requires >= 2.25 for worktree improvements)
39
+ */
40
+ async function checkGitVersion(): Promise<boolean> {
41
+ try {
42
+ const proc = Bun.spawn(["git", "--version"], {
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ });
46
+ const output = await new Response(proc.stdout).text();
47
+ await proc.exited;
48
+
49
+ if (proc.exitCode !== 0) return false;
50
+
51
+ // Parse version from "git version 2.39.1"
52
+ const match = output.match(/git version (\d+)\.(\d+)/);
53
+ if (!match?.[1] || !match[2]) return false;
54
+
55
+ const major = parseInt(match[1], 10);
56
+ const minor = parseInt(match[2], 10);
57
+
58
+ return major > 2 || (major === 2 && minor >= 25);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Check if git repository is initialized
66
+ */
67
+ async function checkGitRepo(): Promise<boolean> {
68
+ try {
69
+ const proc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ });
73
+ await proc.exited;
74
+ return proc.exitCode === 0;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check if Claude authentication is configured
82
+ * Accepts either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN
83
+ */
84
+ function checkClaudeAuth(): Promise<boolean> {
85
+ return Promise.resolve(
86
+ !!process.env.ANTHROPIC_API_KEY || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Check if working directory is clean (no uncommitted changes)
92
+ */
93
+ async function checkCleanWorkingDirectory(): Promise<boolean> {
94
+ try {
95
+ const proc = Bun.spawn(["git", "status", "--porcelain"], {
96
+ stdout: "pipe",
97
+ stderr: "pipe",
98
+ });
99
+ const output = await new Response(proc.stdout).text();
100
+ await proc.exited;
101
+
102
+ if (proc.exitCode !== 0) return false;
103
+
104
+ // Empty output means clean working directory
105
+ return output.trim().length === 0;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Define all prerequisite checks
113
+ */
114
+ function defineChecks(): PrerequisiteCheck[] {
115
+ return [
116
+ {
117
+ name: "git",
118
+ required: true,
119
+ check: () => commandExists("git"),
120
+ errorMessage: "git is not installed. Please install git to use AAD.",
121
+ },
122
+ {
123
+ name: "git-version",
124
+ required: true,
125
+ check: checkGitVersion,
126
+ errorMessage: "git version must be >= 2.25 for worktree support.",
127
+ },
128
+ {
129
+ name: "git-repo",
130
+ required: true,
131
+ check: checkGitRepo,
132
+ errorMessage: "Current directory is not a git repository. Run 'git init' first.",
133
+ },
134
+ {
135
+ name: "claude-auth",
136
+ required: true,
137
+ check: checkClaudeAuth,
138
+ errorMessage: "No authentication configured. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.",
139
+ },
140
+ {
141
+ name: "clean-working-directory",
142
+ required: false,
143
+ check: checkCleanWorkingDirectory,
144
+ errorMessage: "Working directory has uncommitted changes. Commit or stash them before running AAD.",
145
+ },
146
+ ];
147
+ }
148
+
149
+ /**
150
+ * Run all prerequisite checks
151
+ */
152
+ export async function checkPrerequisites(logger?: Logger): Promise<PrerequisiteResult> {
153
+ logger?.debug("Running prerequisite checks");
154
+
155
+ const checks = defineChecks();
156
+ const failures: Array<{ name: string; message: string }> = [];
157
+ const warnings: Array<{ name: string; message: string }> = [];
158
+
159
+ for (const check of checks) {
160
+ try {
161
+ const passed = await check.check();
162
+
163
+ if (!passed) {
164
+ if (check.required) {
165
+ failures.push({ name: check.name, message: check.errorMessage });
166
+ logger?.error({ check: check.name }, check.errorMessage);
167
+ } else {
168
+ warnings.push({ name: check.name, message: check.errorMessage });
169
+ logger?.warn({ check: check.name }, check.errorMessage);
170
+ }
171
+ } else {
172
+ logger?.debug({ check: check.name }, "Prerequisite check passed");
173
+ }
174
+ } catch (error) {
175
+ const errorMessage = `Failed to check ${check.name}: ${error}`;
176
+ if (check.required) {
177
+ failures.push({ name: check.name, message: errorMessage });
178
+ logger?.error({ check: check.name, error }, errorMessage);
179
+ } else {
180
+ warnings.push({ name: check.name, message: errorMessage });
181
+ logger?.warn({ check: check.name, error }, errorMessage);
182
+ }
183
+ }
184
+ }
185
+
186
+ const passed = failures.length === 0;
187
+ logger?.info({ passed, failures: failures.length, warnings: warnings.length }, "Prerequisites check completed");
188
+
189
+ return { passed, failures, warnings };
190
+ }
@@ -83,6 +83,7 @@ export interface Task {
83
83
  endTime?: string;
84
84
  retryCount: number;
85
85
  failureReason?: string;
86
+ previousFailure?: PreviousFailure;
86
87
  repoName?: RepoName;
87
88
  }
88
89
 
@@ -129,6 +130,8 @@ export type TestFramework =
129
130
  | "cargo"
130
131
  | "maven"
131
132
  | "gradle"
133
+ | "playwright"
134
+ | "terraform"
132
135
  | "unknown";
133
136
 
134
137
  // Workspace Info (from project detection)
@@ -150,3 +153,13 @@ export interface PhaseResult {
150
153
  output: string;
151
154
  duration?: number;
152
155
  }
156
+
157
+ /**
158
+ * Previous failure information for retry context
159
+ */
160
+ export interface PreviousFailure {
161
+ phase: string;
162
+ error: string;
163
+ testOutput?: string;
164
+ retryCount: number;
165
+ }