edsger 0.53.0 → 0.54.1

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 (148) hide show
  1. package/README.md +20 -0
  2. package/dist/api/financing.d.ts +47 -0
  3. package/dist/api/financing.js +37 -0
  4. package/dist/api/issues/approval-checker.d.ts +11 -9
  5. package/dist/api/issues/approval-checker.js +30 -41
  6. package/dist/api/issues/status-updater.d.ts +47 -20
  7. package/dist/api/issues/status-updater.js +114 -46
  8. package/dist/api/issues/update-issue.d.ts +5 -0
  9. package/dist/api/issues/update-issue.js +6 -0
  10. package/dist/commands/agent-workflow/processor.js +5 -1
  11. package/dist/commands/checklists/index.d.ts +5 -2
  12. package/dist/commands/checklists/index.js +73 -12
  13. package/dist/commands/checklists/tools.d.ts +14 -7
  14. package/dist/commands/checklists/tools.js +15 -208
  15. package/dist/commands/financing-deck/index.d.ts +8 -0
  16. package/dist/commands/financing-deck/index.js +66 -0
  17. package/dist/commands/find-architecture/index.d.ts +13 -0
  18. package/dist/commands/find-architecture/index.js +41 -0
  19. package/dist/commands/sync-github-issues/index.d.ts +11 -0
  20. package/dist/commands/sync-github-issues/index.js +42 -0
  21. package/dist/commands/sync-sentry-issues/index.d.ts +14 -0
  22. package/dist/commands/sync-sentry-issues/index.js +73 -0
  23. package/dist/commands/workflow/executors/phase-executor.js +6 -4
  24. package/dist/commands/workflow/phase-orchestrator.js +0 -1
  25. package/dist/config/issue-status.d.ts +18 -45
  26. package/dist/config/issue-status.js +21 -107
  27. package/dist/index.js +97 -3
  28. package/dist/phases/app-store-generation/agent.js +2 -1
  29. package/dist/phases/app-store-generation/index.js +11 -3
  30. package/dist/phases/autonomous/index.js +9 -6
  31. package/dist/phases/branch-planning/index.js +1 -2
  32. package/dist/phases/branch-planning/prompts.d.ts +1 -1
  33. package/dist/phases/branch-planning/prompts.js +3 -2
  34. package/dist/phases/bug-fixing/analyzer.js +6 -3
  35. package/dist/phases/bug-fixing/mcp-server.d.ts +18 -1
  36. package/dist/phases/bug-fixing/mcp-server.js +19 -76
  37. package/dist/phases/chat-processor/product-tools.d.ts +5 -8
  38. package/dist/phases/chat-processor/product-tools.js +6 -512
  39. package/dist/phases/chat-processor/tools.d.ts +5 -9
  40. package/dist/phases/chat-processor/tools.js +6 -704
  41. package/dist/phases/code-implementation/branch-pr-creator.js +7 -5
  42. package/dist/phases/code-implementation/index.js +6 -3
  43. package/dist/phases/code-implementation-verification/agent.js +6 -1
  44. package/dist/phases/code-refine/index.js +8 -6
  45. package/dist/phases/code-refine/refine-iteration.js +2 -1
  46. package/dist/phases/code-review/index.js +8 -6
  47. package/dist/phases/code-testing/analyzer.js +11 -8
  48. package/dist/phases/financing-deck/agent.d.ts +1 -0
  49. package/dist/phases/financing-deck/agent.js +96 -0
  50. package/dist/phases/financing-deck/context.d.ts +13 -0
  51. package/dist/phases/financing-deck/context.js +69 -0
  52. package/dist/phases/financing-deck/index.d.ts +15 -0
  53. package/dist/phases/financing-deck/index.js +89 -0
  54. package/dist/phases/financing-deck/prompts.d.ts +2 -0
  55. package/dist/phases/financing-deck/prompts.js +94 -0
  56. package/dist/phases/find-architecture/index.d.ts +44 -0
  57. package/dist/phases/find-architecture/index.js +248 -0
  58. package/dist/phases/find-architecture/prompts.d.ts +31 -0
  59. package/dist/phases/find-architecture/prompts.js +128 -0
  60. package/dist/phases/find-architecture/state.d.ts +21 -0
  61. package/dist/phases/find-architecture/state.js +17 -0
  62. package/dist/phases/find-architecture/types.d.ts +55 -0
  63. package/dist/phases/find-architecture/types.js +69 -0
  64. package/dist/phases/find-bugs/index.js +13 -4
  65. package/dist/phases/find-features/index.js +10 -5
  66. package/dist/phases/find-smells/index.js +10 -3
  67. package/dist/phases/functional-testing/analyzer.js +27 -17
  68. package/dist/phases/functional-testing/http-fallback.d.ts +1 -1
  69. package/dist/phases/functional-testing/http-fallback.js +32 -16
  70. package/dist/phases/functional-testing/mcp-server.d.ts +9 -1
  71. package/dist/phases/functional-testing/mcp-server.js +13 -132
  72. package/dist/phases/growth-analysis/agent.js +2 -2
  73. package/dist/phases/growth-analysis/index.js +9 -3
  74. package/dist/phases/intelligence-analysis/agent.js +2 -2
  75. package/dist/phases/intelligence-analysis/index.js +9 -2
  76. package/dist/phases/issue-analysis/agent.d.ts +9 -1
  77. package/dist/phases/issue-analysis/agent.js +68 -27
  78. package/dist/phases/issue-analysis/context.d.ts +5 -9
  79. package/dist/phases/issue-analysis/context.js +31 -76
  80. package/dist/phases/issue-analysis/index.js +32 -84
  81. package/dist/phases/issue-analysis/outcome.d.ts +3 -33
  82. package/dist/phases/issue-analysis/outcome.js +15 -253
  83. package/dist/phases/issue-analysis/prompts.d.ts +3 -5
  84. package/dist/phases/issue-analysis/prompts.js +45 -158
  85. package/dist/phases/issue-analysis-verification/agent.d.ts +4 -4
  86. package/dist/phases/issue-analysis-verification/agent.js +5 -5
  87. package/dist/phases/issue-analysis-verification/index.d.ts +4 -2
  88. package/dist/phases/issue-analysis-verification/index.js +9 -22
  89. package/dist/phases/issue-analysis-verification/prompts.d.ts +1 -2
  90. package/dist/phases/issue-analysis-verification/prompts.js +21 -46
  91. package/dist/phases/output-contracts.js +66 -78
  92. package/dist/phases/pr-execution/context.d.ts +2 -0
  93. package/dist/phases/pr-execution/context.js +1 -0
  94. package/dist/phases/pr-execution/index.js +28 -19
  95. package/dist/phases/pr-execution/prompts.d.ts +2 -1
  96. package/dist/phases/pr-execution/prompts.js +12 -10
  97. package/dist/phases/pr-resolve/index.js +2 -8
  98. package/dist/phases/pr-splitting/index.js +3 -3
  99. package/dist/phases/pr-splitting/prompts.d.ts +1 -1
  100. package/dist/phases/pr-splitting/prompts.js +3 -2
  101. package/dist/phases/pull-request/creator.js +10 -7
  102. package/dist/phases/pull-request/handler.js +3 -1
  103. package/dist/phases/release-sync/index.js +52 -43
  104. package/dist/phases/run-sheet/index.js +2 -1
  105. package/dist/phases/smoke-test/agent.js +2 -1
  106. package/dist/phases/smoke-test/index.js +4 -1
  107. package/dist/phases/sync-github-issues/index.d.ts +41 -0
  108. package/dist/phases/sync-github-issues/index.js +187 -0
  109. package/dist/phases/sync-github-issues/state.d.ts +26 -0
  110. package/dist/phases/sync-github-issues/state.js +18 -0
  111. package/dist/phases/sync-github-issues/types.d.ts +35 -0
  112. package/dist/phases/sync-github-issues/types.js +6 -0
  113. package/dist/phases/sync-sentry-issues/index.d.ts +29 -0
  114. package/dist/phases/sync-sentry-issues/index.js +153 -0
  115. package/dist/phases/sync-sentry-issues/sentry-client.d.ts +66 -0
  116. package/dist/phases/sync-sentry-issues/sentry-client.js +221 -0
  117. package/dist/phases/sync-sentry-issues/state.d.ts +23 -0
  118. package/dist/phases/sync-sentry-issues/state.js +18 -0
  119. package/dist/phases/sync-sentry-issues/types.d.ts +46 -0
  120. package/dist/phases/sync-sentry-issues/types.js +6 -0
  121. package/dist/phases/sync-shared/mcp.d.ts +81 -0
  122. package/dist/phases/sync-shared/mcp.js +111 -0
  123. package/dist/phases/technical-design/index.js +0 -1
  124. package/dist/phases/test-cases-analysis/agent.js +2 -1
  125. package/dist/phases/test-cases-analysis/index.js +0 -1
  126. package/dist/phases/user-stories-analysis/agent.js +2 -1
  127. package/dist/phases/user-stories-analysis/index.js +0 -1
  128. package/dist/services/coaching/coaching-agent.js +29 -4
  129. package/dist/services/feedbacks.d.ts +1 -1
  130. package/dist/services/repo-config.d.ts +17 -0
  131. package/dist/services/repo-config.js +50 -0
  132. package/dist/skills/phase/issue-analysis/SKILL.md +48 -92
  133. package/dist/skills/phase/issue-analysis-verification/SKILL.md +46 -31
  134. package/dist/tools/bootstrap.d.ts +45 -0
  135. package/dist/tools/bootstrap.js +50 -0
  136. package/dist/types/external-sources.d.ts +22 -0
  137. package/dist/types/external-sources.js +23 -0
  138. package/dist/types/index.d.ts +5 -10
  139. package/dist/types/issues.d.ts +2 -0
  140. package/dist/types/llm-responses.d.ts +1 -14
  141. package/dist/utils/formatters.js +1 -7
  142. package/dist/utils/git-branch-manager-async.js +9 -5
  143. package/dist/utils/git-branch-manager.js +17 -7
  144. package/dist/workspace/workspace-manager.d.ts +10 -0
  145. package/dist/workspace/workspace-manager.js +22 -1
  146. package/package.json +6 -2
  147. package/vitest.config.ts +4 -0
  148. package/.env.local +0 -12
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Types for the find-architecture phase.
3
+ *
4
+ * Where find-smells covers local "the code would be better if changed" findings
5
+ * (refactor candidates, dead code, perf cliffs, etc.), this phase zooms out to
6
+ * **architectural** problems and improvements — module boundaries, layering,
7
+ * coupling, dependency direction, responsibility creep, missing or duplicated
8
+ * abstractions, cross-cutting concerns. Findings span multiple files by nature.
9
+ */
10
+ /**
11
+ * Authoritative list of allowed concern types. Kept as a `const` array so the
12
+ * type and the runtime whitelist can never drift apart.
13
+ */
14
+ export const ARCHITECTURE_CONCERNS = [
15
+ 'layering',
16
+ 'coupling',
17
+ 'cohesion',
18
+ 'cyclic_dependency',
19
+ 'boundary',
20
+ 'duplication',
21
+ 'missing_abstraction',
22
+ 'responsibility_creep',
23
+ 'cross_cutting',
24
+ 'inconsistency',
25
+ 'scalability',
26
+ 'other',
27
+ ];
28
+ export function isArchitectureConcern(value) {
29
+ return (typeof value === 'string' &&
30
+ ARCHITECTURE_CONCERNS.includes(value));
31
+ }
32
+ export function isDeferredFinding(value) {
33
+ if (!value || typeof value !== 'object') {
34
+ return false;
35
+ }
36
+ const v = value;
37
+ return typeof v.title === 'string' && typeof v.reason === 'string';
38
+ }
39
+ export function isScanResult(value) {
40
+ if (!value || typeof value !== 'object') {
41
+ return false;
42
+ }
43
+ const v = value;
44
+ if (typeof v.summary !== 'string' || !Array.isArray(v.findings)) {
45
+ return false;
46
+ }
47
+ const findingsOk = v.findings.every((f) => f &&
48
+ typeof f === 'object' &&
49
+ typeof f.title === 'string' &&
50
+ typeof f.description === 'string' &&
51
+ typeof f.file === 'string');
52
+ if (!findingsOk) {
53
+ return false;
54
+ }
55
+ for (const key of [
56
+ 'deferred_to_bugs',
57
+ 'deferred_to_features',
58
+ 'deferred_to_smells',
59
+ ]) {
60
+ const arr = v[key];
61
+ if (arr === undefined) {
62
+ continue;
63
+ }
64
+ if (!Array.isArray(arr) || !arr.every(isDeferredFinding)) {
65
+ return false;
66
+ }
67
+ }
68
+ return true;
69
+ }
@@ -6,7 +6,7 @@
6
6
  import { query } from '@anthropic-ai/claude-agent-sdk';
7
7
  import { DEFAULT_MODEL } from '../../constants.js';
8
8
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
9
- import { cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
9
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
10
10
  import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
11
11
  import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
12
12
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
@@ -42,15 +42,18 @@ export async function scanForBugs(options) {
42
42
  message: 'Another bug scan is already in progress for this product',
43
43
  };
44
44
  }
45
+ let repoPath;
46
+ let scanSucceeded = false;
45
47
  try {
46
48
  updateFindBugsState(productId, {
47
49
  lastAttemptedAt: new Date().toISOString(),
48
50
  });
49
51
  const workspaceRoot = ensureWorkspaceDir();
50
- // Reuse a single workspace per product so incremental scans don't re-clone.
52
+ // Each run re-clones into a per-product directory and removes it on
53
+ // success. Incremental scope is recovered from the persisted state file
54
+ // (~/.edsger/find-bugs-state/<productId>.json), not from the workspace.
51
55
  const repoKey = `${WORKSPACE_KEY}-${productId}`;
52
- const cloned = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken);
53
- const { repoPath } = cloned;
56
+ ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
54
57
  const branch = options.branch ?? detectDefaultBranch(repoPath);
55
58
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
56
59
  syncRepoToRef(repoPath, { branch }, githubToken);
@@ -72,6 +75,7 @@ export async function scanForBugs(options) {
72
75
  lastScannedAt: new Date().toISOString(),
73
76
  lastError: undefined,
74
77
  });
78
+ scanSucceeded = true;
75
79
  return {
76
80
  status: 'success',
77
81
  message: 'No changes since last scan',
@@ -87,6 +91,7 @@ export async function scanForBugs(options) {
87
91
  }
88
92
  else if (baseSha === headSha) {
89
93
  logSuccess('HEAD unchanged since last scan; nothing to do.');
94
+ scanSucceeded = true;
90
95
  return {
91
96
  status: 'success',
92
97
  message: 'HEAD unchanged since last scan',
@@ -171,6 +176,7 @@ export async function scanForBugs(options) {
171
176
  lastScannedAt: new Date().toISOString(),
172
177
  lastError: undefined,
173
178
  });
179
+ scanSucceeded = true;
174
180
  return {
175
181
  status: 'success',
176
182
  message: `Filed ${created} of ${bugs.length} candidate bugs`,
@@ -190,6 +196,9 @@ export async function scanForBugs(options) {
190
196
  };
191
197
  }
192
198
  finally {
199
+ if (scanSucceeded) {
200
+ cleanupIssueRepo(repoPath);
201
+ }
193
202
  lock.release();
194
203
  }
195
204
  }
@@ -16,7 +16,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
16
16
  import { callMcpEndpoint } from '../../api/mcp-client.js';
17
17
  import { DEFAULT_MODEL } from '../../constants.js';
18
18
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
19
- import { cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
19
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
20
20
  import { detectDefaultBranch } from '../find-shared/git.js';
21
21
  import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
22
22
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
@@ -48,17 +48,18 @@ export async function scanForFeatures(options) {
48
48
  message: 'Another feature discovery run is already in progress for this product',
49
49
  };
50
50
  }
51
+ let repoPath;
52
+ let scanSucceeded = false;
51
53
  try {
52
54
  updateFindFeaturesState(productId, {
53
55
  lastAttemptedAt: new Date().toISOString(),
54
56
  });
55
57
  // Clone / refresh the repo so the model can verify existing functionality
56
- // before proposing new features. Same workspace pattern as find-bugs
57
- // one reusable clone per product, keyed by WORKSPACE_KEY.
58
+ // before proposing new features. Cleaned up after a successful run; the
59
+ // worker re-clones on the next invocation.
58
60
  const workspaceRoot = ensureWorkspaceDir();
59
61
  const repoKey = `${WORKSPACE_KEY}-${productId}`;
60
- const cloned = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken);
61
- const { repoPath } = cloned;
62
+ ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
62
63
  const branch = options.branch ?? detectDefaultBranch(repoPath);
63
64
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
64
65
  syncRepoToRef(repoPath, { branch }, githubToken);
@@ -149,6 +150,7 @@ export async function scanForFeatures(options) {
149
150
  lastRunAt: new Date().toISOString(),
150
151
  lastError: undefined,
151
152
  });
153
+ scanSucceeded = true;
152
154
  return {
153
155
  status: 'success',
154
156
  message: `Filed ${created} of ${opportunities.length} opportunities`,
@@ -167,6 +169,9 @@ export async function scanForFeatures(options) {
167
169
  };
168
170
  }
169
171
  finally {
172
+ if (scanSucceeded) {
173
+ cleanupIssueRepo(repoPath);
174
+ }
170
175
  lock.release();
171
176
  }
172
177
  }
@@ -10,7 +10,7 @@
10
10
  import { query } from '@anthropic-ai/claude-agent-sdk';
11
11
  import { DEFAULT_MODEL } from '../../constants.js';
12
12
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
13
- import { cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
13
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
14
14
  import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
15
15
  import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
16
16
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
@@ -41,14 +41,15 @@ export async function scanForSmells(options) {
41
41
  message: 'Another smell scan is already in progress for this product',
42
42
  };
43
43
  }
44
+ let repoPath;
45
+ let scanSucceeded = false;
44
46
  try {
45
47
  updateFindSmellsState(productId, {
46
48
  lastAttemptedAt: new Date().toISOString(),
47
49
  });
48
50
  const workspaceRoot = ensureWorkspaceDir();
49
51
  const repoKey = `${WORKSPACE_KEY}-${productId}`;
50
- const cloned = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken);
51
- const { repoPath } = cloned;
52
+ ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
52
53
  const branch = options.branch ?? detectDefaultBranch(repoPath);
53
54
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
54
55
  syncRepoToRef(repoPath, { branch }, githubToken);
@@ -70,6 +71,7 @@ export async function scanForSmells(options) {
70
71
  lastScannedAt: new Date().toISOString(),
71
72
  lastError: undefined,
72
73
  });
74
+ scanSucceeded = true;
73
75
  return {
74
76
  status: 'success',
75
77
  message: 'No changes since last scan',
@@ -92,6 +94,7 @@ export async function scanForSmells(options) {
92
94
  lastScannedAt: new Date().toISOString(),
93
95
  lastError: undefined,
94
96
  });
97
+ scanSucceeded = true;
95
98
  return {
96
99
  status: 'success',
97
100
  message: 'HEAD unchanged since last scan',
@@ -201,6 +204,7 @@ export async function scanForSmells(options) {
201
204
  lastError: undefined,
202
205
  });
203
206
  const droppedCount = droppedCategories.reduce((acc, d) => acc + d.count, 0);
207
+ scanSucceeded = true;
204
208
  return {
205
209
  status: 'success',
206
210
  message: `Filed ${created} of ${filteredSmells.length} candidate smells (${droppedCount} dropped by category filter; ${deferredBugs.length} deferred to bugs, ${deferredFeatures.length} to features)`,
@@ -222,6 +226,9 @@ export async function scanForSmells(options) {
222
226
  };
223
227
  }
224
228
  finally {
229
+ if (scanSucceeded) {
230
+ cleanupIssueRepo(repoPath);
231
+ }
225
232
  lock.release();
226
233
  }
227
234
  }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines -- orchestration module with test execution, result processing, and report generation */
2
2
  import { query } from '@anthropic-ai/claude-agent-sdk';
3
- import { updateIssueStatus } from '../../api/issues/index.js';
3
+ import { setPhaseState } from '../../api/issues/index.js';
4
4
  import { getMcpServerUrl, getMcpToken } from '../../auth/auth-store.js';
5
5
  import { DEFAULT_MODEL } from '../../constants.js';
6
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -279,11 +279,21 @@ async function saveTestResults(issueId, testStatus, structuredTestResult, lastAs
279
279
  if (verbose) {
280
280
  logInfo('Saving test results...');
281
281
  }
282
- const statusSaved = await updateIssueStatus({
283
- issueId,
284
- status: testStatus,
285
- verbose,
286
- });
282
+ // testStatus is 'testing_passed' | 'testing_failed'. Under the 2D model
283
+ // both map to a single phase-state update on functional_testing —
284
+ // 'completed' for passed, 'failed' for failed. issues.status stays
285
+ // 'in_progress'; humans decide when to ship.
286
+ const phaseState = testStatus === 'testing_passed' ? 'completed' : 'failed';
287
+ let statusSaved = true;
288
+ try {
289
+ await setPhaseState(issueId, 'functional_testing', phaseState, verbose);
290
+ }
291
+ catch (err) {
292
+ statusSaved = false;
293
+ if (verbose) {
294
+ logError(`Direct setPhaseState failed: ${err instanceof Error ? err.message : String(err)}`);
295
+ }
296
+ }
287
297
  if (!statusSaved) {
288
298
  if (verbose) {
289
299
  logInfo('Direct status update failed, trying HTTP fallback...');
@@ -326,7 +336,6 @@ async function saveTestResults(issueId, testStatus, structuredTestResult, lastAs
326
336
  }
327
337
  return testReportResult;
328
338
  }
329
- // eslint-disable-next-line complexity
330
339
  export const runFunctionalTesting = async (options, config, checklistContext) => {
331
340
  const { issueId, verbose } = options;
332
341
  if (verbose) {
@@ -428,12 +437,13 @@ export const runFunctionalTesting = async (options, config, checklistContext) =>
428
437
  catch (error) {
429
438
  logError(`Functional testing failed: ${error instanceof Error ? error.message : String(error)}`);
430
439
  try {
431
- const errorSaved = await updateIssueStatus({
432
- issueId,
433
- status: 'testing_failed',
434
- verbose,
435
- });
436
- if (!errorSaved) {
440
+ await setPhaseState(issueId, 'functional_testing', 'failed', verbose);
441
+ }
442
+ catch (rpcErr) {
443
+ if (verbose) {
444
+ logError(`Direct setPhaseState failed (${rpcErr instanceof Error ? rpcErr.message : String(rpcErr)}); trying HTTP fallback...`);
445
+ }
446
+ try {
437
447
  await saveFunctionalTestResultsWithRetry({
438
448
  issueId,
439
449
  testStatus: 'testing_failed',
@@ -441,10 +451,10 @@ export const runFunctionalTesting = async (options, config, checklistContext) =>
441
451
  verbose,
442
452
  });
443
453
  }
444
- }
445
- catch (fallbackError) {
446
- if (verbose) {
447
- logError(`❌ Failed to save error status: ${fallbackError}`);
454
+ catch (fallbackError) {
455
+ if (verbose) {
456
+ logError(`❌ Failed to save error status: ${fallbackError}`);
457
+ }
448
458
  }
449
459
  }
450
460
  return {
@@ -5,6 +5,6 @@ interface SaveFunctionalTestResultsOptions {
5
5
  verbose?: boolean;
6
6
  }
7
7
  export declare function saveFunctionalTestResultsViaHttp(options: SaveFunctionalTestResultsOptions): Promise<boolean>;
8
- export declare function verifyTestStatusSaved(issueId: string, verbose?: boolean, expectedStatus?: string): Promise<boolean>;
8
+ export declare function verifyTestStatusSaved(issueId: string, verbose?: boolean, expectedStatus?: SaveFunctionalTestResultsOptions['testStatus']): Promise<boolean>;
9
9
  export declare function saveFunctionalTestResultsWithRetry(options: SaveFunctionalTestResultsOptions, maxRetries?: number): Promise<boolean>;
10
10
  export {};
@@ -1,7 +1,18 @@
1
1
  import { getMcpServerUrl, getMcpToken } from '../../auth/auth-store.js';
2
2
  import { logError, logInfo } from '../../utils/logger.js';
3
+ function legacyTestStatusToPhaseState(testStatus) {
4
+ switch (testStatus) {
5
+ case 'testing_in_progress':
6
+ return 'running';
7
+ case 'testing_passed':
8
+ return 'completed';
9
+ case 'testing_failed':
10
+ return 'failed';
11
+ }
12
+ }
3
13
  export async function saveFunctionalTestResultsViaHttp(options) {
4
- const { issueId, testStatus, testResults: _testResults, verbose } = options;
14
+ const { issueId, testStatus, verbose } = options;
15
+ const phaseState = legacyTestStatusToPhaseState(testStatus);
5
16
  try {
6
17
  if (verbose) {
7
18
  logInfo('🔄 Attempting to save functional test results via HTTP fallback...');
@@ -16,10 +27,11 @@ export async function saveFunctionalTestResultsViaHttp(options) {
16
27
  },
17
28
  body: JSON.stringify({
18
29
  jsonrpc: '2.0',
19
- method: 'issues/update',
30
+ method: 'issues/set_phase_state',
20
31
  params: {
21
32
  issue_id: issueId,
22
- status: testStatus,
33
+ phase: 'functional_testing',
34
+ state: phaseState,
23
35
  },
24
36
  id: Math.random().toString(36).substring(7),
25
37
  }),
@@ -32,7 +44,7 @@ export async function saveFunctionalTestResultsViaHttp(options) {
32
44
  throw new Error(data.error.message || 'HTTP call failed');
33
45
  }
34
46
  if (verbose) {
35
- logInfo(`✅ Functional test results saved successfully via HTTP fallback (status: ${testStatus})`);
47
+ logInfo(`✅ Functional test results saved successfully via HTTP fallback (functional_testing ${phaseState})`);
36
48
  }
37
49
  return true;
38
50
  }
@@ -73,29 +85,33 @@ export async function verifyTestStatusSaved(issueId, verbose, expectedStatus) {
73
85
  throw new Error(data.error.message || 'Verification failed');
74
86
  }
75
87
  const issue = data.result?.issues?.[0];
76
- const actualTestStatus = issue?.status;
77
- if (expectedStatus && actualTestStatus) {
78
- const statusMatches = actualTestStatus === expectedStatus;
88
+ // Look up functional_testing phase status in the workflow array
89
+ const workflow = issue?.workflow;
90
+ const ftPhase = workflow?.find((p) => p.phase === 'functional_testing');
91
+ const actualPhaseState = ftPhase?.status;
92
+ if (expectedStatus && actualPhaseState) {
93
+ const expected = legacyTestStatusToPhaseState(expectedStatus);
94
+ const matches = actualPhaseState === expected;
79
95
  if (verbose) {
80
- if (statusMatches) {
81
- logInfo(`✅ Test status verified - matches expected: ${expectedStatus}`);
96
+ if (matches) {
97
+ logInfo(`✅ Test phase verified - functional_testing.status=${expected}`);
82
98
  }
83
99
  else {
84
- logInfo(`⚠️ Test status exists but differs from expected: ${actualTestStatus} vs ${expectedStatus}`);
100
+ logInfo(`⚠️ Test phase exists but differs - got ${actualPhaseState}, expected ${expected}`);
85
101
  }
86
102
  }
87
- return statusMatches;
103
+ return matches;
88
104
  }
89
- const hasTestStatus = actualTestStatus && actualTestStatus !== 'untested';
105
+ const hasTestPhase = !!actualPhaseState;
90
106
  if (verbose) {
91
- if (hasTestStatus) {
92
- logInfo(`✅ Test status verified - current status: ${actualTestStatus}`);
107
+ if (hasTestPhase) {
108
+ logInfo(`✅ Test phase verified - functional_testing.status=${actualPhaseState}`);
93
109
  }
94
110
  else {
95
- logInfo('⚠️ Test status verification failed - status is untested or not found');
111
+ logInfo('⚠️ Test phase verification failed - functional_testing not in workflow');
96
112
  }
97
113
  }
98
- return hasTestStatus;
114
+ return hasTestPhase;
99
115
  }
100
116
  catch (error) {
101
117
  if (verbose) {
@@ -1 +1,9 @@
1
- export declare const createFunctionalTestingMcpServer: () => import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
1
+ /**
2
+ * Functional-testing MCP server — CLI entry point.
3
+ *
4
+ * Tool implementations live in `edsger-tools`. This shim adapts the CLI's
5
+ * `callMcpEndpoint` transport into the `ToolDeps` shape and reads the
6
+ * legacy env-var credential overrides into the deps `context`.
7
+ */
8
+ import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk';
9
+ export declare const createFunctionalTestingMcpServer: () => McpSdkServerConfigWithInstance;
@@ -1,132 +1,13 @@
1
- import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
2
- import { z } from 'zod';
3
- import { callMcpEndpoint } from '../../api/mcp-client.js';
4
- import { logError } from '../../utils/logger.js';
5
- // Create an SDK MCP server with custom tools for functional testing
6
- export const createFunctionalTestingMcpServer = () => {
7
- return createSdkMcpServer({
8
- name: 'edsger-functional-testing-mcp',
9
- version: '1.0.0',
10
- tools: [
11
- tool('get_issue_testing_info', 'Get comprehensive issue information for functional testing including product, user stories, test cases, and technical design', {
12
- issue_id: z
13
- .string()
14
- .describe('Issue ID to get testing information for'),
15
- }, async (args) => {
16
- try {
17
- // Get issue details
18
- const issueResult = (await callMcpEndpoint('issues/get', {
19
- issue_id: args.issue_id,
20
- }));
21
- if (!issueResult.issues || issueResult.issues.length === 0) {
22
- throw new Error('Issue not found');
23
- }
24
- const issue = issueResult.issues[0];
25
- // Get product details
26
- const productResult = (await callMcpEndpoint('resources/read', {
27
- uri: `product://${issue.product_id}`,
28
- }));
29
- const productText = productResult.contents?.[0]?.text || '{}';
30
- let productInfo;
31
- try {
32
- productInfo = JSON.parse(productText);
33
- }
34
- catch {
35
- productInfo = {
36
- id: issue.product_id,
37
- name: 'Unknown Product',
38
- };
39
- }
40
- // Get user stories
41
- const userStoriesResult = (await callMcpEndpoint('user_stories/list', { issue_id: args.issue_id }));
42
- // Get test cases
43
- const testCasesResult = (await callMcpEndpoint('test_cases/list', {
44
- issue_id: args.issue_id,
45
- }));
46
- const testingInfo = {
47
- issue,
48
- product: productInfo,
49
- user_stories: userStoriesResult.user_stories || [],
50
- test_cases: testCasesResult.test_cases || [],
51
- technical_design: issue.technical_design || null,
52
- current_test_status: issue.status || 'backlog',
53
- };
54
- return {
55
- content: [
56
- {
57
- type: 'text',
58
- text: JSON.stringify(testingInfo, null, 2),
59
- },
60
- ],
61
- };
62
- }
63
- catch (error) {
64
- logError(`Error in get_issue_testing_info: ${error}`);
65
- throw error;
66
- }
67
- }),
68
- tool('update_test_status', 'Update the functional test status of an issue', {
69
- issue_id: z.string().describe('Issue ID to update'),
70
- test_status: z
71
- .enum(['testing_in_progress', 'testing_passed', 'testing_failed'])
72
- .describe('New test status'),
73
- test_results: z
74
- .string()
75
- .optional()
76
- .describe('Optional test results or error details'),
77
- }, async (args) => {
78
- try {
79
- const result = await callMcpEndpoint('issues/update', {
80
- issue_id: args.issue_id,
81
- status: args.test_status,
82
- });
83
- return {
84
- content: [
85
- {
86
- type: 'text',
87
- text: JSON.stringify({
88
- success: true,
89
- issue_id: args.issue_id,
90
- test_status: args.test_status,
91
- test_results: args.test_results || null,
92
- message: `Issue test status updated to: ${args.test_status}`,
93
- result,
94
- }, null, 2),
95
- },
96
- ],
97
- };
98
- }
99
- catch (error) {
100
- logError(`Error updating test status: ${error}`);
101
- throw error;
102
- }
103
- }),
104
- tool('get_environment_config', 'Get testing environment configuration including login credentials', {
105
- environment: z
106
- .string()
107
- .optional()
108
- .default('testing')
109
- .describe('Environment name (testing, staging, production)'),
110
- }, (args) => {
111
- // Get environment variables for testing configuration
112
- const testingConfig = {
113
- environment: args.environment || 'testing',
114
- login_username: process.env.TESTING_LOGIN_USERNAME || '',
115
- login_password: process.env.TESTING_LOGIN_PASSWORD || '',
116
- };
117
- // Validate required configuration
118
- if (!testingConfig.login_username || !testingConfig.login_password) {
119
- throw new Error('Testing credentials not configured. Set TESTING_LOGIN_USERNAME and TESTING_LOGIN_PASSWORD environment variables.');
120
- }
121
- return Promise.resolve({
122
- content: [
123
- {
124
- type: 'text',
125
- text: JSON.stringify(testingConfig, null, 2),
126
- },
127
- ],
128
- });
129
- }),
130
- ],
131
- });
132
- };
1
+ /**
2
+ * Functional-testing MCP server — CLI entry point.
3
+ *
4
+ * Tool implementations live in `edsger-tools`. This shim adapts the CLI's
5
+ * `callMcpEndpoint` transport into the `ToolDeps` shape and reads the
6
+ * legacy env-var credential overrides into the deps `context`.
7
+ */
8
+ import { createFunctionalTestingMcpServer as createFunctionalTestingMcpServerCore } from 'edsger-tools';
9
+ import { getToolDeps } from '../../tools/bootstrap.js';
10
+ export const createFunctionalTestingMcpServer = () => createFunctionalTestingMcpServerCore(getToolDeps(false, {
11
+ testingLoginUsername: process.env.TESTING_LOGIN_USERNAME,
12
+ testingLoginPassword: process.env.TESTING_LOGIN_PASSWORD,
13
+ }));
@@ -38,7 +38,6 @@ function userMessage(content) {
38
38
  async function* prompt(analysisPrompt) {
39
39
  yield userMessage(analysisPrompt);
40
40
  }
41
- // eslint-disable-next-line complexity -- agent loop with message type handling
42
41
  export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, config, verbose, cwd) {
43
42
  let lastAssistantResponse = '';
44
43
  let structuredResult = null;
@@ -69,7 +68,8 @@ export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, co
69
68
  logDebug(`${content.text}`, verbose);
70
69
  }
71
70
  else if (content.type === 'tool_use') {
72
- const desc = content.input?.description || content.input?.command || 'Running...';
71
+ const input = (content.input ?? {});
72
+ const desc = input.description ?? input.command ?? 'Running...';
73
73
  logInfo(`[Turn ${turnCount}] ${content.name}: ${typeof desc === 'string' ? desc.slice(0, 120) : 'Running...'}`);
74
74
  }
75
75
  }
@@ -2,7 +2,7 @@ import { getGitHubConfigByProduct } from '../../api/github.js';
2
2
  import { saveGrowthAnalysis, updateGrowthAnalysis } from '../../api/growth.js';
3
3
  import { generateGrowthVideo, } from '../../services/video/index.js';
4
4
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
5
- import { cloneIssueRepo, ensureWorkspaceDir, } from '../../workspace/workspace-manager.js';
5
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, } from '../../workspace/workspace-manager.js';
6
6
  import { executeGrowthAnalysisQuery } from './agent.js';
7
7
  import { prepareGrowthAnalysisContext } from './context.js';
8
8
  import { createGrowthAnalysisSystemPrompt } from './prompts.js';
@@ -49,15 +49,15 @@ contentSuggestions) {
49
49
  }
50
50
  return plans;
51
51
  }
52
- // eslint-disable-next-line complexity
53
52
  export const analyseGrowth = async (options, config) => {
54
53
  const { productId, verbose, guidance, analysisId } = options;
55
54
  if (verbose) {
56
55
  logInfo(`Starting growth analysis for product ID: ${productId}`);
57
56
  }
57
+ let repoCwd;
58
+ let analysisSucceeded = false;
58
59
  try {
59
60
  // Clone product repo if GitHub is configured
60
- let repoCwd;
61
61
  try {
62
62
  const githubConfig = await getGitHubConfigByProduct(productId, verbose);
63
63
  if (githubConfig.configured &&
@@ -174,6 +174,7 @@ export const analyseGrowth = async (options, config) => {
174
174
  : {}),
175
175
  };
176
176
  });
177
+ analysisSucceeded = true;
177
178
  return {
178
179
  productId,
179
180
  status: 'success',
@@ -193,6 +194,11 @@ export const analyseGrowth = async (options, config) => {
193
194
  contentSuggestions: [],
194
195
  };
195
196
  }
197
+ finally {
198
+ if (analysisSucceeded) {
199
+ cleanupIssueRepo(repoCwd);
200
+ }
201
+ }
196
202
  };
197
203
  /**
198
204
  * Run async tasks with a concurrency limit.