@ronkovic/aad 0.3.9 → 0.5.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 (132) hide show
  1. package/README.md +332 -14
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
  4. package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
  6. package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
  7. package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
  8. package/src/__tests__/e2e/status-e2e.test.ts +227 -0
  9. package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
  10. package/src/__tests__/helpers/index.ts +6 -0
  11. package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
  12. package/src/__tests__/helpers/mock-logger.ts +36 -0
  13. package/src/__tests__/helpers/wait-helpers.ts +34 -0
  14. package/src/__tests__/integration/pipeline.test.ts +3 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  16. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
  17. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
  18. package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
  19. package/src/modules/cli/__tests__/resume.test.ts +4 -0
  20. package/src/modules/cli/__tests__/run.test.ts +37 -0
  21. package/src/modules/cli/__tests__/status.test.ts +1 -0
  22. package/src/modules/cli/app.ts +2 -0
  23. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  24. package/src/modules/cli/commands/cleanup.ts +26 -11
  25. package/src/modules/cli/commands/resume.ts +14 -8
  26. package/src/modules/cli/commands/run.ts +70 -8
  27. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  28. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  29. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  30. package/src/modules/dashboard/routes/sse.ts +3 -2
  31. package/src/modules/dashboard/server.ts +1 -0
  32. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  33. package/src/modules/dashboard/ui/dashboard.html +640 -349
  34. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  35. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  36. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  37. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  38. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  39. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  40. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  41. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  42. package/src/modules/git-workspace/branch-manager.ts +24 -3
  43. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  44. package/src/modules/git-workspace/git-exec.ts +3 -2
  45. package/src/modules/git-workspace/index.ts +10 -1
  46. package/src/modules/git-workspace/merge-service.ts +36 -2
  47. package/src/modules/git-workspace/pr-manager.ts +278 -0
  48. package/src/modules/git-workspace/template-copy.ts +302 -0
  49. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  50. package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
  51. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  52. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  53. package/src/modules/planning/planning.service.ts +16 -2
  54. package/src/modules/planning/project-detection.ts +4 -1
  55. package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
  56. package/src/modules/process-manager/process-manager.ts +2 -1
  57. package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
  58. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  59. package/src/modules/task-execution/executor.ts +163 -4
  60. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  61. package/src/modules/task-execution/phases/merge.ts +44 -2
  62. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  63. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  64. package/src/modules/task-queue/dispatcher.ts +96 -3
  65. package/src/shared/__tests__/config.test.ts +30 -0
  66. package/src/shared/__tests__/events.test.ts +42 -16
  67. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  68. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  69. package/src/shared/config.ts +10 -0
  70. package/src/shared/events.ts +5 -0
  71. package/src/shared/memory-check.ts +2 -2
  72. package/src/shared/prerequisites.ts +190 -0
  73. package/src/shared/shutdown-handler.ts +12 -5
  74. package/src/shared/types.ts +25 -0
  75. package/templates/CLAUDE.md +122 -0
  76. package/templates/settings.json +117 -0
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -186,22 +186,31 @@ describe("EventBus", () => {
186
186
  test("listener exception does not affect other listeners", () => {
187
187
  let received: AADEvent | undefined;
188
188
 
189
- eventBus.on("worker:idle", () => {
190
- throw new Error("Listener error");
191
- });
192
-
193
- eventBus.on("worker:idle", (event) => {
194
- received = event;
195
- });
196
-
197
- const event: AADEvent = {
198
- type: "worker:idle",
199
- workerId: createWorkerId("w1"),
200
- };
201
-
202
- eventBus.emit(event);
203
-
204
- expect(received).toEqual(event);
189
+ // Mock console.error to suppress error output
190
+ const originalConsoleError = console.error;
191
+ console.error = () => {};
192
+
193
+ try {
194
+ eventBus.on("worker:idle", () => {
195
+ throw new Error("Listener error");
196
+ });
197
+
198
+ eventBus.on("worker:idle", (event) => {
199
+ received = event;
200
+ });
201
+
202
+ const event: AADEvent = {
203
+ type: "worker:idle",
204
+ workerId: createWorkerId("w1"),
205
+ };
206
+
207
+ eventBus.emit(event);
208
+
209
+ expect(received).toEqual(event);
210
+ } finally {
211
+ // Restore console.error
212
+ console.error = originalConsoleError;
213
+ }
205
214
  });
206
215
 
207
216
  test("emits wildcard * for all events", () => {
@@ -493,4 +502,21 @@ describe("EventBus", () => {
493
502
 
494
503
  expect(received).toEqual(event);
495
504
  });
505
+
506
+ test("emits and receives execution:skipped event", () => {
507
+ let received: AADEvent | undefined;
508
+ eventBus.on("execution:skipped", (event) => {
509
+ received = event;
510
+ });
511
+
512
+ const event: AADEvent = {
513
+ type: "execution:skipped",
514
+ taskId: createTaskId("t1"),
515
+ reason: "Tests already pass",
516
+ };
517
+
518
+ eventBus.emit(event);
519
+
520
+ expect(received).toEqual(event);
521
+ });
496
522
  });
@@ -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
+ });
@@ -135,4 +135,100 @@ describe("shutdown-handler", () => {
135
135
  // exitFn called only once due to debounce
136
136
  expect(exitFn).toHaveBeenCalledTimes(1);
137
137
  });
138
+
139
+ test("displays resume command example when using fs persistence", async () => {
140
+ const tasks = [makeTask("t1", "running")];
141
+ const runState: RunState = {
142
+ runId,
143
+ parentBranch: "main",
144
+ totalTasks: 1,
145
+ pending: 0,
146
+ running: 1,
147
+ completed: 0,
148
+ failed: 0,
149
+ startTime: new Date().toISOString(),
150
+ };
151
+
152
+ const exitFn = mock(() => {});
153
+ const stores = createMockStores(tasks, runState);
154
+ const logger = createMockLogger();
155
+
156
+ // Capture console output
157
+ const consoleLogSpy = mock(() => {});
158
+ const originalLog = console.log;
159
+ console.log = consoleLogSpy;
160
+
161
+ try {
162
+ installShutdownHandler({
163
+ runId,
164
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
165
+ logger,
166
+ exitFn,
167
+ persistMode: "fs",
168
+ });
169
+
170
+ // Simulate SIGINT
171
+ process.emit("SIGINT");
172
+
173
+ // Wait for async handler
174
+ await new Promise((r) => setTimeout(r, 50));
175
+
176
+ // Verify resume command was logged
177
+ const calls = (consoleLogSpy as any).mock.calls as string[][];
178
+ const resumeLog = calls.find((args) =>
179
+ args.some((arg) => typeof arg === "string" && arg.includes("aad resume"))
180
+ );
181
+ expect(resumeLog).toBeDefined();
182
+ } finally {
183
+ console.log = originalLog;
184
+ }
185
+ });
186
+
187
+ test("does not display resume command when using memory persistence", async () => {
188
+ const tasks = [makeTask("t1", "running")];
189
+ const runState: RunState = {
190
+ runId,
191
+ parentBranch: "main",
192
+ totalTasks: 1,
193
+ pending: 0,
194
+ running: 1,
195
+ completed: 0,
196
+ failed: 0,
197
+ startTime: new Date().toISOString(),
198
+ };
199
+
200
+ const exitFn = mock(() => {});
201
+ const stores = createMockStores(tasks, runState);
202
+ const logger = createMockLogger();
203
+
204
+ // Capture console output
205
+ const consoleLogSpy = mock(() => {});
206
+ const originalLog = console.log;
207
+ console.log = consoleLogSpy;
208
+
209
+ try {
210
+ installShutdownHandler({
211
+ runId,
212
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
213
+ logger,
214
+ exitFn,
215
+ persistMode: "memory",
216
+ });
217
+
218
+ // Simulate SIGINT
219
+ process.emit("SIGINT");
220
+
221
+ // Wait for async handler
222
+ await new Promise((r) => setTimeout(r, 50));
223
+
224
+ // Verify resume command was NOT logged
225
+ const calls = (consoleLogSpy as any).mock.calls as string[][];
226
+ const resumeLog = calls.find((args) =>
227
+ args.some((arg) => typeof arg === "string" && arg.includes("aad resume"))
228
+ );
229
+ expect(resumeLog).toBeUndefined();
230
+ } finally {
231
+ console.log = originalLog;
232
+ }
233
+ });
138
234
  });
@@ -25,6 +25,8 @@ const configSchema = z.object({
25
25
  }),
26
26
  debug: z.boolean(),
27
27
  adaptiveEffort: z.boolean(),
28
+ skipCompleted: z.boolean(),
29
+ strictTdd: z.boolean(),
28
30
  teams: z.object({
29
31
  splitter: z.boolean(),
30
32
  reviewer: z.boolean(),
@@ -38,6 +40,9 @@ const configSchema = z.object({
38
40
  port: z.number().int().min(1).max(65535),
39
41
  host: z.string(),
40
42
  }),
43
+ git: z.object({
44
+ autoPush: z.boolean(),
45
+ }),
41
46
  repos: z.array(z.object({
42
47
  name: z.string().optional(),
43
48
  path: z.string(),
@@ -98,6 +103,8 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
98
103
  },
99
104
  debug: parseBoolOrDefault(env.DEBUG, false),
100
105
  adaptiveEffort: parseBoolOrDefault(env.AAD_ADAPTIVE_EFFORT, false),
106
+ skipCompleted: parseBoolOrDefault(env.AAD_SKIP_COMPLETED, true),
107
+ strictTdd: parseBoolOrDefault(env.AAD_STRICT_TDD, false),
101
108
  teams: {
102
109
  splitter: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_SPLITTER, false),
103
110
  reviewer: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_REVIEW, false),
@@ -111,6 +118,9 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
111
118
  port: parseIntOrDefault(env.AAD_DASHBOARD_PORT, 7333),
112
119
  host: env.AAD_DASHBOARD_HOST ?? "localhost",
113
120
  },
121
+ git: {
122
+ autoPush: parseBoolOrDefault(env.AAD_GIT_AUTO_PUSH, true),
123
+ },
114
124
  repos: env.AAD_REPOS
115
125
  ? env.AAD_REPOS.split(",").map((p) => ({ path: p.trim() }))
116
126
  : undefined,
@@ -122,6 +122,11 @@ export type AADEvent =
122
122
  type: "execution:merge:conflict";
123
123
  taskId: TaskId;
124
124
  conflictedFiles: string[];
125
+ }
126
+ | {
127
+ type: "execution:skipped";
128
+ taskId: TaskId;
129
+ reason: string;
125
130
  };
126
131
 
127
132
  export type EventType = AADEvent["type"] | "*";
@@ -64,8 +64,8 @@ export async function getMemoryStatus(): Promise<MemoryStatus> {
64
64
  const purgablePages = parseInt(purgableMatch?.[1] ?? "0", 10);
65
65
 
66
66
  // Calculate memory in GB
67
- const bytesToGB = (bytes: number) => bytes / (1024 * 1024 * 1024);
68
- const pagesToGB = (pages: number) => bytesToGB(pages * pageSize);
67
+ const bytesToGB = (bytes: number): number => bytes / (1024 * 1024 * 1024);
68
+ const pagesToGB = (pages: number): number => bytesToGB(pages * pageSize);
69
69
 
70
70
  // Available = free + inactive + speculative + purgeable
71
71
  // macOS reclaims inactive/purgeable pages on demand — they are effectively free
@@ -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
+ }
@@ -15,6 +15,7 @@ export interface ShutdownHandlerOptions {
15
15
  };
16
16
  logger: pino.Logger;
17
17
  exitFn?: (code: number) => void;
18
+ persistMode?: "memory" | "fs";
18
19
  }
19
20
 
20
21
  let _shuttingDown = false;
@@ -56,7 +57,7 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
56
57
  const { runId, stores, logger, exitFn = (code: number) => process.exit(code) } = options;
57
58
  let saving = false;
58
59
 
59
- const handleShutdown = async (reason: string) => {
60
+ const handleShutdown = async (reason: string): Promise<void> => {
60
61
  const now = Date.now();
61
62
  // Debounce: ignore signals within 1s
62
63
  if (now - _lastSignalTime < 1000) return;
@@ -95,7 +96,13 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
95
96
  });
96
97
  }
97
98
 
98
- logger.info({ runId, pending, completed, failed }, "State saved. Use --resume to continue.");
99
+ logger.info({ runId, pending, completed, failed }, "State saved.");
100
+
101
+ // Display resume command example when using fs persistence
102
+ if (options.persistMode === "fs") {
103
+ console.log(`\n⚠️ Run interrupted. To resume:\n`);
104
+ console.log(` aad resume ${runId}\n`);
105
+ }
99
106
  } catch (err) {
100
107
  logger.error({ err }, "Failed to save state during shutdown");
101
108
  }
@@ -103,9 +110,9 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
103
110
  exitFn(1);
104
111
  };
105
112
 
106
- const onSigterm = () => { void handleShutdown("SIGTERM"); };
107
- const onSigint = () => { void handleShutdown("SIGINT"); };
108
- const onUncaught = (err: Error) => { void handleShutdown(`uncaughtException: ${err.message}`); };
113
+ const onSigterm = (): void => { void handleShutdown("SIGTERM"); };
114
+ const onSigint = (): void => { void handleShutdown("SIGINT"); };
115
+ const onUncaught = (err: Error): void => { void handleShutdown(`uncaughtException: ${err.message}`); };
109
116
 
110
117
  process.on("SIGTERM", onSigterm);
111
118
  process.on("SIGINT", onSigint);
@@ -54,6 +54,8 @@ export interface TaskExecutionResult {
54
54
  duration: number;
55
55
  output?: string;
56
56
  error?: string;
57
+ skipped?: boolean;
58
+ phasesExecuted?: string[];
57
59
  }
58
60
 
59
61
  // RepoName branded type
@@ -83,6 +85,7 @@ export interface Task {
83
85
  endTime?: string;
84
86
  retryCount: number;
85
87
  failureReason?: string;
88
+ previousFailure?: PreviousFailure;
86
89
  repoName?: RepoName;
87
90
  }
88
91
 
@@ -109,6 +112,16 @@ export interface RunState {
109
112
  endTime?: string;
110
113
  }
111
114
 
115
+ // Run Metrics
116
+ export interface RunMetrics {
117
+ totalTasks: number;
118
+ completedTasks: number;
119
+ failedTasks: number;
120
+ skippedTasks: number;
121
+ totalDuration: number;
122
+ averageTaskDuration: number;
123
+ }
124
+
112
125
  // Task Plan
113
126
  export interface TaskPlan {
114
127
  runId: RunId;
@@ -129,6 +142,8 @@ export type TestFramework =
129
142
  | "cargo"
130
143
  | "maven"
131
144
  | "gradle"
145
+ | "playwright"
146
+ | "terraform"
132
147
  | "unknown";
133
148
 
134
149
  // Workspace Info (from project detection)
@@ -150,3 +165,13 @@ export interface PhaseResult {
150
165
  output: string;
151
166
  duration?: number;
152
167
  }
168
+
169
+ /**
170
+ * Previous failure information for retry context
171
+ */
172
+ export interface PreviousFailure {
173
+ phase: string;
174
+ error: string;
175
+ testOutput?: string;
176
+ retryCount: number;
177
+ }