edsger 0.51.0 → 0.53.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 (188) hide show
  1. package/.claude/settings.local.json +23 -3
  2. package/.env.local +12 -0
  3. package/dist/commands/find-smells/index.d.ts +21 -0
  4. package/dist/commands/find-smells/index.js +65 -0
  5. package/dist/index.js +29 -0
  6. package/dist/phases/find-bugs/index.js +7 -92
  7. package/dist/phases/find-bugs/state.d.ts +10 -35
  8. package/dist/phases/find-bugs/state.js +12 -120
  9. package/dist/phases/find-features/index.js +16 -83
  10. package/dist/phases/find-features/prompts.d.ts +7 -1
  11. package/dist/phases/find-features/prompts.js +31 -11
  12. package/dist/phases/find-features/state.d.ts +15 -19
  13. package/dist/phases/find-features/state.js +17 -89
  14. package/dist/phases/find-features/types.d.ts +1 -1
  15. package/dist/phases/find-shared/git.d.ts +24 -0
  16. package/dist/phases/find-shared/git.js +60 -0
  17. package/dist/phases/find-shared/mcp.d.ts +33 -0
  18. package/dist/phases/find-shared/mcp.js +69 -0
  19. package/dist/phases/find-shared/scan-state.d.ts +33 -0
  20. package/dist/phases/find-shared/scan-state.js +112 -0
  21. package/dist/phases/find-smells/index.d.ts +47 -0
  22. package/dist/phases/find-smells/index.js +278 -0
  23. package/dist/phases/find-smells/prompts.d.ts +30 -0
  24. package/dist/phases/find-smells/prompts.js +129 -0
  25. package/dist/phases/find-smells/state.d.ts +21 -0
  26. package/dist/phases/find-smells/state.js +17 -0
  27. package/dist/phases/find-smells/types.d.ts +51 -0
  28. package/dist/phases/find-smells/types.js +64 -0
  29. package/dist/phases/pr-execution/context.js +40 -32
  30. package/dist/phases/pr-splitting/context.js +18 -13
  31. package/dist/utils/github-repo-info.d.ts +13 -1
  32. package/dist/utils/github-repo-info.js +32 -6
  33. package/package.json +1 -1
  34. package/vitest.config.ts +2 -0
  35. package/dist/api/__tests__/app-store.test.d.ts +0 -7
  36. package/dist/api/__tests__/app-store.test.js +0 -60
  37. package/dist/api/__tests__/intelligence.test.d.ts +0 -11
  38. package/dist/api/__tests__/intelligence.test.js +0 -315
  39. package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
  40. package/dist/api/features/__tests__/feature-utils.test.js +0 -370
  41. package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
  42. package/dist/api/features/__tests__/status-updater.test.js +0 -88
  43. package/dist/api/features/approval-checker.d.ts +0 -20
  44. package/dist/api/features/approval-checker.js +0 -152
  45. package/dist/api/features/batch-operations.d.ts +0 -17
  46. package/dist/api/features/batch-operations.js +0 -100
  47. package/dist/api/features/feature-utils.d.ts +0 -23
  48. package/dist/api/features/feature-utils.js +0 -80
  49. package/dist/api/features/get-feature.d.ts +0 -5
  50. package/dist/api/features/get-feature.js +0 -21
  51. package/dist/api/features/index.d.ts +0 -8
  52. package/dist/api/features/index.js +0 -10
  53. package/dist/api/features/status-updater.d.ts +0 -41
  54. package/dist/api/features/status-updater.js +0 -122
  55. package/dist/api/features/test-cases.d.ts +0 -29
  56. package/dist/api/features/test-cases.js +0 -110
  57. package/dist/api/features/update-feature.d.ts +0 -20
  58. package/dist/api/features/update-feature.js +0 -83
  59. package/dist/api/features/user-stories.d.ts +0 -21
  60. package/dist/api/features/user-stories.js +0 -88
  61. package/dist/commands/agent-workflow/feature-worker.d.ts +0 -14
  62. package/dist/commands/agent-workflow/feature-worker.js +0 -65
  63. package/dist/commands/build/__tests__/build.test.d.ts +0 -5
  64. package/dist/commands/build/__tests__/build.test.js +0 -206
  65. package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
  66. package/dist/commands/build/__tests__/detect-project.test.js +0 -160
  67. package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
  68. package/dist/commands/build/__tests__/run-build.test.js +0 -433
  69. package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
  70. package/dist/commands/intelligence/__tests__/command.test.js +0 -48
  71. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
  72. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
  73. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
  74. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
  75. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
  76. package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
  77. package/dist/commands/workflow/core/feature-filter.d.ts +0 -16
  78. package/dist/commands/workflow/core/feature-filter.js +0 -47
  79. package/dist/commands/workflow/feature-coordinator.d.ts +0 -18
  80. package/dist/commands/workflow/feature-coordinator.js +0 -161
  81. package/dist/config/__tests__/config.test.d.ts +0 -4
  82. package/dist/config/__tests__/config.test.js +0 -286
  83. package/dist/config/__tests__/feature-status.test.d.ts +0 -4
  84. package/dist/config/__tests__/feature-status.test.js +0 -111
  85. package/dist/config/feature-status.d.ts +0 -56
  86. package/dist/config/feature-status.js +0 -130
  87. package/dist/errors/__tests__/index.test.d.ts +0 -4
  88. package/dist/errors/__tests__/index.test.js +0 -349
  89. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
  90. package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
  91. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
  92. package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
  93. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
  94. package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
  95. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
  96. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
  97. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
  98. package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
  99. package/dist/phases/feature-analysis/agent.d.ts +0 -13
  100. package/dist/phases/feature-analysis/agent.js +0 -112
  101. package/dist/phases/feature-analysis/context.d.ts +0 -24
  102. package/dist/phases/feature-analysis/context.js +0 -138
  103. package/dist/phases/feature-analysis/index.d.ts +0 -8
  104. package/dist/phases/feature-analysis/index.js +0 -199
  105. package/dist/phases/feature-analysis/outcome.d.ts +0 -40
  106. package/dist/phases/feature-analysis/outcome.js +0 -280
  107. package/dist/phases/feature-analysis/prompts.d.ts +0 -10
  108. package/dist/phases/feature-analysis/prompts.js +0 -212
  109. package/dist/phases/feature-analysis-verification/agent.d.ts +0 -33
  110. package/dist/phases/feature-analysis-verification/agent.js +0 -124
  111. package/dist/phases/feature-analysis-verification/index.d.ts +0 -25
  112. package/dist/phases/feature-analysis-verification/index.js +0 -92
  113. package/dist/phases/feature-analysis-verification/prompts.d.ts +0 -10
  114. package/dist/phases/feature-analysis-verification/prompts.js +0 -100
  115. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
  116. package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
  117. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
  118. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
  119. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
  120. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
  121. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
  122. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
  123. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
  124. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
  125. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
  126. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
  127. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
  128. package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
  129. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
  130. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
  131. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
  132. package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
  133. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
  134. package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
  135. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
  136. package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
  137. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
  138. package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
  139. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
  140. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
  141. package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
  142. package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
  143. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
  144. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
  145. package/dist/phases/run-sheet/render.d.ts +0 -60
  146. package/dist/phases/run-sheet/render.js +0 -297
  147. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
  148. package/dist/phases/smoke-test/__tests__/agent.test.js +0 -84
  149. package/dist/phases/smoke-test/__tests__/github.test.d.ts +0 -9
  150. package/dist/phases/smoke-test/__tests__/github.test.js +0 -120
  151. package/dist/phases/smoke-test/__tests__/snapshot.test.d.ts +0 -8
  152. package/dist/phases/smoke-test/__tests__/snapshot.test.js +0 -93
  153. package/dist/phases/smoke-test/github.d.ts +0 -54
  154. package/dist/phases/smoke-test/github.js +0 -101
  155. package/dist/phases/smoke-test/snapshot.d.ts +0 -27
  156. package/dist/phases/smoke-test/snapshot.js +0 -157
  157. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
  158. package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
  159. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
  160. package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
  161. package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
  162. package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
  163. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
  164. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
  165. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
  166. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
  167. package/dist/services/lifecycle-agent/index.d.ts +0 -24
  168. package/dist/services/lifecycle-agent/index.js +0 -25
  169. package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
  170. package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
  171. package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
  172. package/dist/services/lifecycle-agent/transition-rules.js +0 -184
  173. package/dist/services/lifecycle-agent/types.d.ts +0 -190
  174. package/dist/services/lifecycle-agent/types.js +0 -12
  175. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
  176. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
  177. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
  178. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
  179. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
  180. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
  181. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
  182. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
  183. package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
  184. package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
  185. package/dist/types/features.d.ts +0 -35
  186. package/dist/types/features.js +0 -1
  187. package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
  188. package/dist/workspace/__tests__/workspace-manager.test.js +0 -52
@@ -1,8 +1,28 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(npx tsc:*)",
5
- "Bash(npm run:*)"
6
- ]
4
+ "Read(//Users/steven/development/edsger/**)",
5
+ "Bash(npm run build)",
6
+ "Bash(node:*)",
7
+ "Bash(git add:*)",
8
+ "Bash(git commit:*)",
9
+ "Bash(ls:*)",
10
+ "Bash(cat:*)",
11
+ "Bash(npm run typecheck:*)",
12
+ "Bash(git diff:*)",
13
+ "WebSearch",
14
+ "WebFetch(domain:supabase.com)",
15
+ "Bash(npm install:*)",
16
+ "Bash(grep:*)",
17
+ "Bash(npx supabase gen types typescript --help:*)",
18
+ "Bash(git -C /Users/steven/development/edsger status)",
19
+ "Bash(git -C /Users/steven/development/edsger diff)",
20
+ "Bash(git -C /Users/steven/development/edsger log --oneline -5)",
21
+ "Bash(git -C /Users/steven/development/edsger add supabase/migrations/20251231000000_drop_unused_views.sql)",
22
+ "Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views that are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
23
+ "Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views\nthat are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
24
+ ],
25
+ "deny": [],
26
+ "ask": []
7
27
  }
8
28
  }
package/.env.local ADDED
@@ -0,0 +1,12 @@
1
+ OPENAI_API_KEY=
2
+ TRIGGER_SECRET_KEY=
3
+ RECALL_AI_API_KEY=
4
+ RECALL_WEBHOOK_SECRET=
5
+ NEXT_PUBLIC_APP_URL=
6
+ RESEND_API_KEY=
7
+ EMAIL_FROM=
8
+ GITHUB_APP_ID=
9
+ GITHUB_APP_PRIVATE_KEY=
10
+ GITHUB_APP_SLUG=
11
+ DEEPGRAM_API_KEY=
12
+ DEEPGRAM_PROJECT_ID=
@@ -0,0 +1,21 @@
1
+ /**
2
+ * CLI command: edsger find-smells <productId>
3
+ * Audits the product's repository for code smells (refactor candidates, perf
4
+ * cliffs, dead code, etc.) and files each new finding as an issue.
5
+ */
6
+ import { type SmellCategory } from '../../phases/find-smells/types.js';
7
+ export interface FindSmellsCliOptions {
8
+ full?: boolean;
9
+ branch?: string;
10
+ maxFiles?: number;
11
+ categories?: SmellCategory[];
12
+ verbose?: boolean;
13
+ }
14
+ /**
15
+ * Parse and validate `--categories` from the CLI. Throws on invalid input so
16
+ * commander's option-parser rejects the run before any work starts. Empty
17
+ * input is treated as "no filter" (returns undefined) rather than an error —
18
+ * matches user intuition for `--categories=`.
19
+ */
20
+ export declare function parseCategoriesOption(raw: string): SmellCategory[] | undefined;
21
+ export declare function runFindSmells(productId: string, options: FindSmellsCliOptions): Promise<void>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CLI command: edsger find-smells <productId>
3
+ * Audits the product's repository for code smells (refactor candidates, perf
4
+ * cliffs, dead code, etc.) and files each new finding as an issue.
5
+ */
6
+ import { getGitHubConfigByProduct } from '../../api/github.js';
7
+ import { scanForSmells } from '../../phases/find-smells/index.js';
8
+ import { isSmellCategory, SMELL_CATEGORIES, } from '../../phases/find-smells/types.js';
9
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
10
+ /**
11
+ * Parse and validate `--categories` from the CLI. Throws on invalid input so
12
+ * commander's option-parser rejects the run before any work starts. Empty
13
+ * input is treated as "no filter" (returns undefined) rather than an error —
14
+ * matches user intuition for `--categories=`.
15
+ */
16
+ export function parseCategoriesOption(raw) {
17
+ const tokens = raw
18
+ .split(',')
19
+ .map((s) => s.trim())
20
+ .filter((s) => s.length > 0);
21
+ if (tokens.length === 0) {
22
+ return undefined;
23
+ }
24
+ const invalid = tokens.filter((t) => !isSmellCategory(t));
25
+ if (invalid.length > 0) {
26
+ throw new Error(`--categories: unknown value(s): ${invalid.join(', ')}. ` +
27
+ `Allowed: ${SMELL_CATEGORIES.join(', ')}`);
28
+ }
29
+ // Dedup while preserving order so the error message and downstream behaviour
30
+ // are deterministic.
31
+ return Array.from(new Set(tokens));
32
+ }
33
+ export async function runFindSmells(productId, options) {
34
+ const { full, branch, maxFiles, categories, verbose } = options;
35
+ logInfo(`Starting smell scan for product ${productId}`);
36
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
37
+ if (!githubConfig.configured ||
38
+ !githubConfig.token ||
39
+ !githubConfig.owner ||
40
+ !githubConfig.repo) {
41
+ logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
42
+ process.exit(1);
43
+ }
44
+ const result = await scanForSmells({
45
+ productId,
46
+ githubToken: githubConfig.token,
47
+ owner: githubConfig.owner,
48
+ repo: githubConfig.repo,
49
+ full,
50
+ branch,
51
+ maxFiles,
52
+ categories,
53
+ verbose,
54
+ });
55
+ if (result.status === 'success') {
56
+ logSuccess(result.message);
57
+ if (result.summary) {
58
+ logInfo(result.summary);
59
+ }
60
+ }
61
+ else {
62
+ logError(result.message);
63
+ process.exit(1);
64
+ }
65
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { runCodeReview } from './commands/code-review/index.js';
15
15
  import { runConfigGet, runConfigList, runConfigSet, runConfigUnset, } from './commands/config/index.js';
16
16
  import { runFindBugs } from './commands/find-bugs/index.js';
17
17
  import { runFindFeatures } from './commands/find-features/index.js';
18
+ import { parseCategoriesOption, runFindSmells, } from './commands/find-smells/index.js';
18
19
  import { runGrowthAnalysis } from './commands/growth-analysis/index.js';
19
20
  import { runInit } from './commands/init/index.js';
20
21
  import { runIntelligence } from './commands/intelligence/index.js';
@@ -26,6 +27,8 @@ import { runRunSheetCommand } from './commands/run-sheet/index.js';
26
27
  import { runSmokeTestCommand } from './commands/smoke-test/index.js';
27
28
  import { runTaskWorker } from './commands/task-worker/index.js';
28
29
  import { runWorkflow } from './commands/workflow/index.js';
30
+ import { DEFAULT_MAX_FILES as FIND_SMELLS_DEFAULT_MAX_FILES } from './phases/find-smells/index.js';
31
+ import { SMELL_CATEGORIES, } from './phases/find-smells/types.js';
29
32
  import { logError, logInfo } from './utils/logger.js';
30
33
  // Get package.json version dynamically
31
34
  // eslint-disable-next-line @typescript-eslint/naming-convention -- ESM __filename/__dirname polyfill
@@ -414,6 +417,32 @@ program
414
417
  }
415
418
  });
416
419
  // ============================================================
420
+ // Subcommand: edsger find-smells <productId>
421
+ // ============================================================
422
+ program
423
+ .command('find-smells <productId>')
424
+ .description("AI-audit a product's repository for code smells (refactor candidates, perf cliffs, dead code, type-safety gaps) and file each new finding as an issue")
425
+ .option('--full', 'Force a full scan even if previous-scan state exists')
426
+ .option('--branch <name>', 'Branch to scan (defaults to repo default branch)')
427
+ .option('--max-files <n>', `Upper bound on files the auditor may Read (default ${FIND_SMELLS_DEFAULT_MAX_FILES})`, (value) => {
428
+ const n = parseInt(value, 10);
429
+ if (Number.isNaN(n) || n <= 0) {
430
+ throw new Error('--max-files must be a positive integer');
431
+ }
432
+ return n;
433
+ })
434
+ .option('--categories <list>', `Comma-separated category filter (${SMELL_CATEGORIES.join(',')})`, parseCategoriesOption)
435
+ .option('-v, --verbose', 'Verbose output')
436
+ .action(async (productId, opts) => {
437
+ try {
438
+ await runFindSmells(productId, opts);
439
+ }
440
+ catch (error) {
441
+ logError(error instanceof Error ? error.message : String(error));
442
+ process.exit(1);
443
+ }
444
+ });
445
+ // ============================================================
417
446
  // Subcommand: edsger pr-resolve <productId>
418
447
  // ============================================================
419
448
  program
@@ -4,11 +4,11 @@
4
4
  * incremental, scoped to commits since the previous successful scan.
5
5
  */
6
6
  import { query } from '@anthropic-ai/claude-agent-sdk';
7
- import { execFileSync } from 'child_process';
8
- import { callMcpEndpoint } from '../../api/mcp-client.js';
9
7
  import { DEFAULT_MODEL } from '../../constants.js';
10
8
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
11
9
  import { cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
10
+ import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
11
+ import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
12
12
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
13
13
  import { createFindBugsSystemPrompt, createFindBugsUserPrompt, } from './prompts.js';
14
14
  import { acquireFindBugsLock, loadFindBugsState, updateFindBugsState, } from './state.js';
@@ -193,97 +193,12 @@ export async function scanForBugs(options) {
193
193
  lock.release();
194
194
  }
195
195
  }
196
- function gitRevParse(repoPath, ref) {
197
- return execFileSync('git', ['rev-parse', ref], {
198
- cwd: repoPath,
199
- encoding: 'utf-8',
200
- }).trim();
201
- }
202
- function detectDefaultBranch(repoPath) {
203
- try {
204
- const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: repoPath, encoding: 'utf-8' }).trim();
205
- // ref is like "origin/main"
206
- return ref.replace(/^origin\//, '');
207
- }
208
- catch {
209
- return 'main';
210
- }
211
- }
212
- function isAncestor(repoPath, ancestor, descendant) {
213
- try {
214
- execFileSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
215
- cwd: repoPath,
216
- stdio: 'pipe',
217
- });
218
- return true;
219
- }
220
- catch {
221
- return false;
222
- }
223
- }
224
- function listChangedPaths(repoPath, baseSha, headSha) {
225
- try {
226
- const out = execFileSync('git', ['diff', '--name-only', `${baseSha}..${headSha}`], { cwd: repoPath, encoding: 'utf-8' });
227
- return out
228
- .split('\n')
229
- .map((s) => s.trim())
230
- .filter((s) => s.length > 0);
231
- }
232
- catch {
233
- return [];
234
- }
235
- }
236
- async function fetchProductBasics(productId) {
237
- try {
238
- const result = (await callMcpEndpoint('resources/read', {
239
- uri: `product://${productId}`,
240
- }));
241
- const text = result.contents?.[0]?.text || '{}';
242
- const parsed = JSON.parse(text);
243
- return {
244
- name: parsed.name || productId,
245
- description: parsed.description,
246
- };
247
- }
248
- catch {
249
- return { name: productId };
250
- }
251
- }
252
- async function fetchOpenIssues(productId) {
253
- try {
254
- const result = (await callMcpEndpoint('issues/list', {
255
- product_id: productId,
256
- }));
257
- const all = result.issues || [];
258
- // Dedup is only useful against issues that are still actionable.
259
- return all.filter((i) => !isTerminalStatus(i.status));
260
- }
261
- catch (error) {
262
- logWarning(`Could not load existing issues for dedup: ${error instanceof Error ? error.message : String(error)}`);
263
- return [];
264
- }
265
- }
266
- function isTerminalStatus(status) {
267
- return (status === 'shipped' ||
268
- status === 'archived' ||
269
- status === 'cancelled' ||
270
- status === 'closed' ||
271
- status === 'completed');
272
- }
273
196
  async function createIssueForBug(productId, bug) {
274
- const description = formatIssueDescription(bug);
275
- try {
276
- const result = (await callMcpEndpoint('issues/create', {
277
- product_id: productId,
278
- name: bug.title,
279
- description,
280
- }));
281
- return result.issue?.id || result.id || null;
282
- }
283
- catch (error) {
284
- logError(`Failed to create issue for "${bug.title}": ${error instanceof Error ? error.message : String(error)}`);
285
- return null;
286
- }
197
+ return createIssue({
198
+ productId,
199
+ title: bug.title,
200
+ description: formatIssueDescription(bug),
201
+ });
287
202
  }
288
203
  function formatIssueDescription(bug) {
289
204
  const location = bug.line ? `${bug.file}:${bug.line}` : bug.file;
@@ -1,44 +1,19 @@
1
1
  /**
2
- * Per-product scan state persisted under `~/.edsger/find-bugs-state/<productId>.json`.
2
+ * Per-product scan state for find-bugs. Storage at
3
+ * `~/.edsger/find-bugs-state/<productId>.json`.
3
4
  *
4
- * Covers three concerns:
5
- *
6
- * 1. **Incremental scope**: `lastScannedCommitSha` tells the next run where to
7
- * diff from. Only updated on a successful audit.
8
- * 2. **Failure tracking**: `lastAttemptedAt` / `lastError` are updated even on
9
- * failure so an orchestrating agent can back off instead of looping on a
10
- * permanent misconfiguration (e.g. missing GitHub install).
11
- * 3. **Concurrency**: a lock file next to the state file gates simultaneous
12
- * runs for the same product — they share one workspace clone.
13
- *
14
- * Known limitation: state is machine-local. A scan started on machine A and
15
- * next on machine B will look like a first run. Tracked for a future migration
16
- * to a DB-backed store (see the v0.2 followups).
5
+ * Schema is bug-scan-specific (incremental commit sha tracking + failure
6
+ * recording); the file/lock machinery is shared via createScanStateModule.
17
7
  */
8
+ import { type LockHandle } from '../find-shared/scan-state.js';
9
+ export type { LockHandle };
18
10
  export interface FindBugsState {
19
11
  lastScannedCommitSha?: string;
20
12
  lastScannedAt?: string;
21
13
  lastAttemptedAt?: string;
22
14
  lastError?: string;
23
15
  }
24
- export declare function loadFindBugsState(productId: string): FindBugsState;
25
- /**
26
- * Atomic write: stage to a tempfile, then rename. A crash mid-write leaves the
27
- * previous state intact instead of truncating it.
28
- */
29
- export declare function saveFindBugsState(productId: string, state: FindBugsState): void;
30
- /**
31
- * Merge new fields into the stored state. Fields not mentioned are preserved.
32
- */
33
- export declare function updateFindBugsState(productId: string, patch: Partial<FindBugsState>): FindBugsState;
34
- export interface LockHandle {
35
- release: () => void;
36
- }
37
- /**
38
- * Acquire an exclusive lock for this product's scan state.
39
- *
40
- * Uses O_EXCL on a lockfile, which is atomic on local POSIX filesystems (good
41
- * enough — the workspace clone is also machine-local). Returns null if already
42
- * held. Stale locks older than `staleAfterMs` are reclaimed.
43
- */
44
- export declare function acquireFindBugsLock(productId: string, staleAfterMs?: number): LockHandle | null;
16
+ export declare const loadFindBugsState: (productId: string) => FindBugsState;
17
+ export declare const saveFindBugsState: (productId: string, state: FindBugsState) => void;
18
+ export declare const updateFindBugsState: (productId: string, patch: Partial<FindBugsState>) => FindBugsState;
19
+ export declare const acquireFindBugsLock: (productId: string, staleAfterMs?: number) => LockHandle | null;
@@ -1,121 +1,13 @@
1
1
  /**
2
- * Per-product scan state persisted under `~/.edsger/find-bugs-state/<productId>.json`.
3
- *
4
- * Covers three concerns:
5
- *
6
- * 1. **Incremental scope**: `lastScannedCommitSha` tells the next run where to
7
- * diff from. Only updated on a successful audit.
8
- * 2. **Failure tracking**: `lastAttemptedAt` / `lastError` are updated even on
9
- * failure so an orchestrating agent can back off instead of looping on a
10
- * permanent misconfiguration (e.g. missing GitHub install).
11
- * 3. **Concurrency**: a lock file next to the state file gates simultaneous
12
- * runs for the same product — they share one workspace clone.
13
- *
14
- * Known limitation: state is machine-local. A scan started on machine A and
15
- * next on machine B will look like a first run. Tracked for a future migration
16
- * to a DB-backed store (see the v0.2 followups).
17
- */
18
- import { existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'fs';
19
- import { homedir } from 'os';
20
- import { join } from 'path';
21
- function stateDir() {
22
- return join(homedir(), '.edsger', 'find-bugs-state');
23
- }
24
- function safeProductId(productId) {
25
- // productId is a UUID controlled upstream; still strip any slashes defensively.
26
- return productId.replace(/[^a-zA-Z0-9_-]/g, '_');
27
- }
28
- function statePath(productId) {
29
- return join(stateDir(), `${safeProductId(productId)}.json`);
30
- }
31
- function lockPath(productId) {
32
- return join(stateDir(), `${safeProductId(productId)}.lock`);
33
- }
34
- function ensureStateDir() {
35
- const dir = stateDir();
36
- if (!existsSync(dir)) {
37
- mkdirSync(dir, { recursive: true });
38
- }
39
- return dir;
40
- }
41
- export function loadFindBugsState(productId) {
42
- const p = statePath(productId);
43
- if (!existsSync(p)) {
44
- return {};
45
- }
46
- try {
47
- const raw = readFileSync(p, 'utf-8');
48
- const parsed = JSON.parse(raw);
49
- return parsed && typeof parsed === 'object' ? parsed : {};
50
- }
51
- catch {
52
- return {};
53
- }
54
- }
55
- /**
56
- * Atomic write: stage to a tempfile, then rename. A crash mid-write leaves the
57
- * previous state intact instead of truncating it.
58
- */
59
- export function saveFindBugsState(productId, state) {
60
- ensureStateDir();
61
- const finalPath = statePath(productId);
62
- const tmpPath = `${finalPath}.${process.pid}.tmp`;
63
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
64
- renameSync(tmpPath, finalPath);
65
- }
66
- /**
67
- * Merge new fields into the stored state. Fields not mentioned are preserved.
68
- */
69
- export function updateFindBugsState(productId, patch) {
70
- const merged = { ...loadFindBugsState(productId), ...patch };
71
- saveFindBugsState(productId, merged);
72
- return merged;
73
- }
74
- /**
75
- * Acquire an exclusive lock for this product's scan state.
76
- *
77
- * Uses O_EXCL on a lockfile, which is atomic on local POSIX filesystems (good
78
- * enough — the workspace clone is also machine-local). Returns null if already
79
- * held. Stale locks older than `staleAfterMs` are reclaimed.
80
- */
81
- export function acquireFindBugsLock(productId, staleAfterMs = 60 * 60 * 1000) {
82
- ensureStateDir();
83
- const lock = lockPath(productId);
84
- if (existsSync(lock)) {
85
- try {
86
- const raw = readFileSync(lock, 'utf-8');
87
- const parsed = JSON.parse(raw);
88
- const age = Date.now() - new Date(parsed.acquiredAt ?? 0).getTime();
89
- if (age < staleAfterMs) {
90
- return null;
91
- }
92
- // Stale — reclaim. A run crashing without cleanup (SIGKILL, OOM) is the
93
- // only way to reach here. Logging is the caller's job.
94
- rmSync(lock, { force: true });
95
- }
96
- catch {
97
- // Corrupt lockfile — also reclaim.
98
- rmSync(lock, { force: true });
99
- }
100
- }
101
- try {
102
- // wx = fail if exists, create otherwise. Atomic against a racing peer.
103
- const fd = openSync(lock, 'wx');
104
- writeFileSync(fd, JSON.stringify({ acquiredAt: new Date().toISOString(), pid: process.pid }));
105
- // Note: we don't hold the fd; the lock is the file's existence.
106
- // Close is implicit via the writeFileSync result on this fd.
107
- return {
108
- release: () => {
109
- try {
110
- unlinkSync(lock);
111
- }
112
- catch {
113
- // Already gone — fine.
114
- }
115
- },
116
- };
117
- }
118
- catch {
119
- return null;
120
- }
121
- }
2
+ * Per-product scan state for find-bugs. Storage at
3
+ * `~/.edsger/find-bugs-state/<productId>.json`.
4
+ *
5
+ * Schema is bug-scan-specific (incremental commit sha tracking + failure
6
+ * recording); the file/lock machinery is shared via createScanStateModule.
7
+ */
8
+ import { createScanStateModule, } from '../find-shared/scan-state.js';
9
+ const m = createScanStateModule({ dirName: 'find-bugs-state' });
10
+ export const loadFindBugsState = m.load;
11
+ export const saveFindBugsState = m.save;
12
+ export const updateFindBugsState = m.update;
13
+ export const acquireFindBugsLock = m.acquireLock;
@@ -13,11 +13,12 @@
13
13
  * code, we care what exists there.
14
14
  */
15
15
  import { query } from '@anthropic-ai/claude-agent-sdk';
16
- import { execFileSync } from 'child_process';
17
16
  import { callMcpEndpoint } from '../../api/mcp-client.js';
18
17
  import { DEFAULT_MODEL } from '../../constants.js';
19
18
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
20
19
  import { cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
20
+ import { detectDefaultBranch } from '../find-shared/git.js';
21
+ import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
21
22
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
22
23
  import { createFindFeaturesSystemPrompt, createFindFeaturesUserPrompt, } from './prompts.js';
23
24
  import { acquireFindFeaturesLock, updateFindFeaturesState } from './state.js';
@@ -61,7 +62,6 @@ export async function scanForFeatures(options) {
61
62
  const branch = options.branch ?? detectDefaultBranch(repoPath);
62
63
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
63
64
  syncRepoToRef(repoPath, { branch }, githubToken);
64
- const headSha = gitRevParse(repoPath, 'HEAD');
65
65
  const sinceIso = resolveSinceIso(since);
66
66
  const product = await fetchProductBasics(productId);
67
67
  const [feedbacks, intelligenceReports, existingIssues] = await Promise.all([
@@ -70,21 +70,11 @@ export async function scanForFeatures(options) {
70
70
  fetchOpenIssues(productId),
71
71
  ]);
72
72
  logInfo(`Loaded ${feedbacks.length} feedbacks (since ${since}), ${intelligenceReports.length} reports, ${existingIssues.length} open issues`);
73
- if (feedbacks.length === 0 && intelligenceReports.length === 0) {
74
- logWarning('No feedback or intelligence reports in scope — nothing to synthesise.');
75
- updateFindFeaturesState(productId, {
76
- lastRunAt: new Date().toISOString(),
77
- lastScannedCommitSha: headSha,
78
- lastError: undefined,
79
- });
80
- return {
81
- status: 'success',
82
- message: 'No feedback or intelligence reports available',
83
- opportunitiesFound: 0,
84
- issuesCreated: 0,
85
- };
73
+ const codeOnlyMode = feedbacks.length === 0 && intelligenceReports.length === 0;
74
+ if (codeOnlyMode) {
75
+ logInfo('No feedback or intelligence reports in scope — running code-only analysis on the repo.');
86
76
  }
87
- const systemPrompt = createFindFeaturesSystemPrompt();
77
+ const systemPrompt = createFindFeaturesSystemPrompt({ codeOnlyMode });
88
78
  const userPrompt = createFindFeaturesUserPrompt({
89
79
  productName: product.name,
90
80
  productDescription: product.description,
@@ -99,6 +89,7 @@ export async function scanForFeatures(options) {
99
89
  maxSuggestions,
100
90
  sinceWindow: since,
101
91
  focusReportId,
92
+ codeOnlyMode,
102
93
  });
103
94
  let lastAssistantResponse = '';
104
95
  let discoveryResult = null;
@@ -150,11 +141,12 @@ export async function scanForFeatures(options) {
150
141
  logSuccess(`Filed issue ${issueId}: ${opp.title}`);
151
142
  }
152
143
  }
144
+ // The previous schema captured `lastSeenFeedbackId / lastSeenReportId /
145
+ // lastScannedCommitSha` here, but the reader side never used them — kept
146
+ // it lean and only persist the run-status fields the worker actually
147
+ // reads on retry. See find-features/state.ts for the schema rationale.
153
148
  updateFindFeaturesState(productId, {
154
149
  lastRunAt: new Date().toISOString(),
155
- lastSeenFeedbackId: feedbacks[0]?.id,
156
- lastSeenReportId: intelligenceReports[0]?.id,
157
- lastScannedCommitSha: headSha,
158
150
  lastError: undefined,
159
151
  });
160
152
  return {
@@ -178,21 +170,6 @@ export async function scanForFeatures(options) {
178
170
  lock.release();
179
171
  }
180
172
  }
181
- function gitRevParse(repoPath, ref) {
182
- return execFileSync('git', ['rev-parse', ref], {
183
- cwd: repoPath,
184
- encoding: 'utf-8',
185
- }).trim();
186
- }
187
- function detectDefaultBranch(repoPath) {
188
- try {
189
- const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: repoPath, encoding: 'utf-8' }).trim();
190
- return ref.replace(/^origin\//, '');
191
- }
192
- catch {
193
- return 'main';
194
- }
195
- }
196
173
  /**
197
174
  * Convert a user-friendly "since" value (e.g. "30d", "12h", ISO timestamp) to
198
175
  * an ISO string usable as a lower bound. Falls back to 90 days ago on parse
@@ -275,56 +252,12 @@ async function fetchIntelligenceReports(productId, focusReportId) {
275
252
  return [];
276
253
  }
277
254
  }
278
- async function fetchProductBasics(productId) {
279
- try {
280
- const result = (await callMcpEndpoint('resources/read', {
281
- uri: `product://${productId}`,
282
- }));
283
- const text = result.contents?.[0]?.text || '{}';
284
- const parsed = JSON.parse(text);
285
- return {
286
- name: parsed.name || productId,
287
- description: parsed.description,
288
- };
289
- }
290
- catch {
291
- return { name: productId };
292
- }
293
- }
294
- async function fetchOpenIssues(productId) {
295
- try {
296
- const result = (await callMcpEndpoint('issues/list', {
297
- product_id: productId,
298
- }));
299
- const all = result.issues || [];
300
- return all.filter((i) => !isTerminalStatus(i.status));
301
- }
302
- catch (error) {
303
- logWarning(`Could not load existing issues for dedup: ${error instanceof Error ? error.message : String(error)}`);
304
- return [];
305
- }
306
- }
307
- function isTerminalStatus(status) {
308
- return (status === 'shipped' ||
309
- status === 'archived' ||
310
- status === 'cancelled' ||
311
- status === 'closed' ||
312
- status === 'completed');
313
- }
314
255
  async function createIssueForOpportunity(productId, opp) {
315
- const description = formatIssueDescription(opp);
316
- try {
317
- const result = (await callMcpEndpoint('issues/create', {
318
- product_id: productId,
319
- name: opp.title,
320
- description,
321
- }));
322
- return result.issue?.id || result.id || null;
323
- }
324
- catch (error) {
325
- logError(`Failed to create issue for "${opp.title}": ${error instanceof Error ? error.message : String(error)}`);
326
- return null;
327
- }
256
+ return createIssue({
257
+ productId,
258
+ title: opp.title,
259
+ description: formatIssueDescription(opp),
260
+ });
328
261
  }
329
262
  function formatIssueDescription(opp) {
330
263
  const evidenceLine = opp.supporting_evidence_ids.length > 0
@@ -6,7 +6,11 @@
6
6
  * "opportunities" that are really just discoverability issues on existing
7
7
  * functionality.
8
8
  */
9
- export declare function createFindFeaturesSystemPrompt(): string;
9
+ export interface FindFeaturesSystemPromptParams {
10
+ /** True when no feedback/reports were available; agent must derive opportunities purely from code. */
11
+ codeOnlyMode?: boolean;
12
+ }
13
+ export declare function createFindFeaturesSystemPrompt(params?: FindFeaturesSystemPromptParams): string;
10
14
  export interface FindFeaturesUserPromptParams {
11
15
  productName: string;
12
16
  productDescription?: string;
@@ -33,5 +37,7 @@ export interface FindFeaturesUserPromptParams {
33
37
  sinceWindow: string;
34
38
  /** Optional narrowing to a specific intelligence report's context. */
35
39
  focusReportId?: string;
40
+ /** True when the run has no feedback/reports and must analyse the codebase only. */
41
+ codeOnlyMode?: boolean;
36
42
  }
37
43
  export declare function createFindFeaturesUserPrompt(params: FindFeaturesUserPromptParams): string;