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
@@ -6,14 +6,20 @@
6
6
  * "opportunities" that are really just discoverability issues on existing
7
7
  * functionality.
8
8
  */
9
- export function createFindFeaturesSystemPrompt() {
10
- return `You are a senior product manager synthesising **opportunities worth building** for a product. You are reading user feedback, competitive intelligence, and the product's current codebase, then picking the few ideas with the strongest evidence.
9
+ export function createFindFeaturesSystemPrompt(params = {}) {
10
+ const { codeOnlyMode = false } = params;
11
+ const evidenceClause = codeOnlyMode
12
+ ? `2. **Evidence from the codebase** — since no user feedback or reports are available this run, opportunities must be grounded in concrete observations of the code: half-finished flows, TODO/FIXME clusters, obvious gaps next to existing functionality, missing standard capabilities for this kind of product. Cite file paths (e.g. \`src/foo/bar.ts:42\`) as your evidence in \`supporting_evidence_ids\`.`
13
+ : `2. **Evidence beyond one voice** — backed by multiple feedbacks, a competitor gap, or a measurable trend. Cite specific IDs.`;
14
+ return `You are a senior product manager synthesising **opportunities worth building** for a product. You are reading user feedback, competitive intelligence, and the product's current codebase, then picking the few ideas with the strongest evidence.${codeOnlyMode
15
+ ? '\n\nThis run has **no user feedback and no intelligence reports** in scope. Derive opportunities purely from the codebase: look for half-built flows, TODO/FIXME clusters, missing standard capabilities, and natural extensions of existing features. Cite file paths as evidence.'
16
+ : ''}
11
17
 
12
18
  You have read-only access to the repository via Read/Grep/Glob. Use it — the single most common failure mode of this role is proposing a feature that already exists but is badly discoverable. Before filing an opportunity, grep the code for the feature you're about to propose; if it's already implemented, either reframe the opportunity as a discoverability fix or drop it.
13
19
 
14
20
  **What counts as an opportunity worth filing**:
15
21
  1. **Clear user outcome** — you can articulate in one sentence what changes for the user.
16
- 2. **Evidence beyond one voice** — backed by multiple feedbacks, a competitor gap, or a measurable trend. Cite specific IDs.
22
+ ${evidenceClause}
17
23
  3. **Feasible in this product's shape** — adjacent to existing workflows, not a pivot. Verify against the code.
18
24
  4. **Not already in the backlog** — cross-check the existing-issues list; skip anything that overlaps, even if the wording differs.
19
25
  5. **Not already in the code** — grep before you file. If the feature exists but is hidden, the opportunity is "improve discoverability of X", not "add X".
@@ -28,11 +34,11 @@ You have read-only access to the repository via Read/Grep/Glob. Use it — the s
28
34
 
29
35
  **Discipline**:
30
36
  - Prefer **fewer**, **better-evidenced** ideas over a long list. Ten mediocre suggestions dilute the good ones.
31
- - Each opportunity must cite \`supporting_evidence_ids\` — the feedback IDs or report IDs that support it. Zero IDs = you're speculating; cut it.
32
- - Confidence rubric:
33
- - high = 3+ independent user signals **or** a clear competitor parity gap backed by a report
34
- - medium = 2 signals, or a strong theme in one report
35
- - low = a single thoughtful signal that still deserves surfacing
37
+ - Each opportunity must cite \`supporting_evidence_ids\` — feedback IDs, report IDs, ${codeOnlyMode ? 'or **file paths** (e.g. `src/foo/bar.ts:42`) when running in code-only mode' : 'or, when no PM signal exists, file paths in the form `src/foo/bar.ts:42`'}. Zero evidence = you're speculating; cut it.
38
+ - Confidence rubric${codeOnlyMode ? ' (code-only mode)' : ''}:
39
+ - high = ${codeOnlyMode ? 'a half-finished flow with multiple TODOs **or** an obvious gap next to existing functionality with clear product framing' : '3+ independent user signals **or** a clear competitor parity gap backed by a report'}
40
+ - medium = ${codeOnlyMode ? 'a noticeable gap or natural extension supported by 2+ code references' : '2 signals, or a strong theme in one report'}
41
+ - low = ${codeOnlyMode ? 'a single thoughtful code observation that still deserves surfacing' : 'a single thoughtful signal that still deserves surfacing'}
36
42
  - **Deduplication**: skip findings that overlap any existing open issue. Be conservative — rephrased overlap is still overlap.
37
43
  - **Verification**: for each candidate, grep the repo for the functionality. Note in the opportunity description what you checked and why you're confident it's genuinely missing.
38
44
 
@@ -62,7 +68,7 @@ End your response with a single JSON code block in this exact shape:
62
68
  If nothing meets the bar, emit the JSON with an empty \`opportunities\` array and a summary that says so plainly.`;
63
69
  }
64
70
  export function createFindFeaturesUserPrompt(params) {
65
- const { productName, productDescription, feedbacks, intelligenceReports, existingIssues, minClusterSize, maxSuggestions, sinceWindow, focusReportId, } = params;
71
+ const { productName, productDescription, feedbacks, intelligenceReports, existingIssues, minClusterSize, maxSuggestions, sinceWindow, focusReportId, codeOnlyMode = false, } = params;
66
72
  const productBlock = productDescription
67
73
  ? `**Product**: ${productName}\n${productDescription}`
68
74
  : `**Product**: ${productName}`;
@@ -99,7 +105,21 @@ ${issuesBlock}
99
105
  - Surface **at most ${maxSuggestions} opportunities**. Rank by confidence + evidence strength; drop the weakest.
100
106
  - A "theme" needs at least ${minClusterSize} independent feedback mentions to count as high-confidence (solo mentions may still appear at low confidence if they're specific and concrete).
101
107
 
102
- ## How to work
108
+ ${codeOnlyMode
109
+ ? `## How to work (code-only mode — no feedback or reports this run)
110
+ 1. Orient: Glob top-level source dirs, Read the README and any docs/ index, list the main routes/screens/commands. Goal: build a one-paragraph mental model of what the product already does.
111
+ 2. Identify candidate gaps from the code itself:
112
+ - Half-finished flows (UI exists but handler is a stub, route returns 501, TODO/FIXME clusters).
113
+ - Standard capabilities a product of this shape would normally have but doesn't (search, export, filtering, bulk actions, audit logs, settings, empty-state guidance, error recovery).
114
+ - Natural extensions of existing features (one-off where a list/multi version is the obvious next step).
115
+ - Inconsistencies (feature available in one entity but not its peers).
116
+ 3. For each candidate: (a) cross-check the existing-issues list for overlap, (b) grep more carefully to confirm the gap is real, not just code you missed.
117
+ 4. Rank by signal strength. Keep at most ${maxSuggestions}; cut hard. Skip pure refactors and tech debt.
118
+ 5. For each survivor: write a clear user outcome, cite **file paths** in \`supporting_evidence_ids\` (e.g. \`src/exports/index.ts:42\`), set \`source\` to \`code_analysis\`, and pick a confidence level honestly using the code-only rubric.
119
+ 6. Produce the final JSON described in the system prompt. If nothing meets the bar, emit an empty \`opportunities\` array with a summary that says so plainly.
120
+
121
+ Begin.`
122
+ : `## How to work
103
123
  1. Start with a lightweight orientation pass on the repo: Glob for top-level source dirs, Read the README and any docs/ index, grep for obvious domain concepts. Goal: know what the product already does before you synthesise what it should do.
104
124
  2. Scan feedbacks. Group by theme. Note which themes have ≥${minClusterSize} mentions.
105
125
  3. Scan intelligence reports. Note any gaps, trends, or competitor parity points.
@@ -108,7 +128,7 @@ ${issuesBlock}
108
128
  6. For each: write a clear user outcome, cite evidence IDs, pick a confidence level honestly. In the description's "Rough scope" section, note what you verified in code (e.g. "confirmed no CSV export code path under src/exports/").
109
129
  7. Produce the final JSON described in the system prompt.
110
130
 
111
- Begin.`;
131
+ Begin.`}`;
112
132
  }
113
133
  function truncate(s, n) {
114
134
  if (s.length <= n) {
@@ -1,29 +1,25 @@
1
1
  /**
2
- * Per-product discovery state persisted under
2
+ * Per-product discovery state for find-features. Storage at
3
3
  * `~/.edsger/find-features-state/<productId>.json`.
4
4
  *
5
- * Unlike find-bugs (which tracks a commit sha for diff-based incremental
6
- * scans), feature discovery is time-based: we record when we last ran and
7
- * what the most-recent feedback we'd seen was, so the next run can focus on
8
- * genuinely new material instead of re-clustering the same themes.
5
+ * Tracks only run-status fields (last successful run, last attempt, last
6
+ * error) feature discovery uses a time-based "since" window passed at
7
+ * call time, so there's no per-run cursor to persist. The earlier schema
8
+ * captured `lastSeenFeedbackId / lastSeenReportId / lastScannedCommitSha`
9
+ * but they were never read; trimmed in favour of a leaner schema. If a
10
+ * future run wants to skip already-clustered material, restore them and
11
+ * wire the read side at the same time.
9
12
  *
10
- * Also shares the same atomic-write + exclusive-lock machinery as find-bugs.
13
+ * File layout + lock semantics shared via createScanStateModule.
11
14
  */
15
+ import { type LockHandle } from '../find-shared/scan-state.js';
16
+ export type { LockHandle };
12
17
  export interface FindFeaturesState {
13
18
  lastRunAt?: string;
14
19
  lastAttemptedAt?: string;
15
20
  lastError?: string;
16
- /** Most recent feedback id seen by the previous successful run. */
17
- lastSeenFeedbackId?: string;
18
- /** Most recent intelligence report id seen. */
19
- lastSeenReportId?: string;
20
- /** HEAD sha of the repo when the last successful run read it. Metadata only. */
21
- lastScannedCommitSha?: string;
22
21
  }
23
- export declare function loadFindFeaturesState(productId: string): FindFeaturesState;
24
- export declare function saveFindFeaturesState(productId: string, state: FindFeaturesState): void;
25
- export declare function updateFindFeaturesState(productId: string, patch: Partial<FindFeaturesState>): FindFeaturesState;
26
- export interface LockHandle {
27
- release: () => void;
28
- }
29
- export declare function acquireFindFeaturesLock(productId: string, staleAfterMs?: number): LockHandle | null;
22
+ export declare const loadFindFeaturesState: (productId: string) => FindFeaturesState;
23
+ export declare const saveFindFeaturesState: (productId: string, state: FindFeaturesState) => void;
24
+ export declare const updateFindFeaturesState: (productId: string, patch: Partial<FindFeaturesState>) => FindFeaturesState;
25
+ export declare const acquireFindFeaturesLock: (productId: string, staleAfterMs?: number) => LockHandle | null;
@@ -1,94 +1,22 @@
1
1
  /**
2
- * Per-product discovery state persisted under
2
+ * Per-product discovery state for find-features. Storage at
3
3
  * `~/.edsger/find-features-state/<productId>.json`.
4
4
  *
5
- * Unlike find-bugs (which tracks a commit sha for diff-based incremental
6
- * scans), feature discovery is time-based: we record when we last ran and
7
- * what the most-recent feedback we'd seen was, so the next run can focus on
8
- * genuinely new material instead of re-clustering the same themes.
5
+ * Tracks only run-status fields (last successful run, last attempt, last
6
+ * error) feature discovery uses a time-based "since" window passed at
7
+ * call time, so there's no per-run cursor to persist. The earlier schema
8
+ * captured `lastSeenFeedbackId / lastSeenReportId / lastScannedCommitSha`
9
+ * but they were never read; trimmed in favour of a leaner schema. If a
10
+ * future run wants to skip already-clustered material, restore them and
11
+ * wire the read side at the same time.
9
12
  *
10
- * Also shares the same atomic-write + exclusive-lock machinery as find-bugs.
13
+ * File layout + lock semantics shared via createScanStateModule.
11
14
  */
12
- import { existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'fs';
13
- import { homedir } from 'os';
14
- import { join } from 'path';
15
- function stateDir() {
16
- return join(homedir(), '.edsger', 'find-features-state');
17
- }
18
- function safeProductId(productId) {
19
- return productId.replace(/[^a-zA-Z0-9_-]/g, '_');
20
- }
21
- function statePath(productId) {
22
- return join(stateDir(), `${safeProductId(productId)}.json`);
23
- }
24
- function lockPath(productId) {
25
- return join(stateDir(), `${safeProductId(productId)}.lock`);
26
- }
27
- function ensureStateDir() {
28
- const dir = stateDir();
29
- if (!existsSync(dir)) {
30
- mkdirSync(dir, { recursive: true });
31
- }
32
- return dir;
33
- }
34
- export function loadFindFeaturesState(productId) {
35
- const p = statePath(productId);
36
- if (!existsSync(p)) {
37
- return {};
38
- }
39
- try {
40
- const raw = readFileSync(p, 'utf-8');
41
- const parsed = JSON.parse(raw);
42
- return parsed && typeof parsed === 'object' ? parsed : {};
43
- }
44
- catch {
45
- return {};
46
- }
47
- }
48
- export function saveFindFeaturesState(productId, state) {
49
- ensureStateDir();
50
- const finalPath = statePath(productId);
51
- const tmpPath = `${finalPath}.${process.pid}.tmp`;
52
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
53
- renameSync(tmpPath, finalPath);
54
- }
55
- export function updateFindFeaturesState(productId, patch) {
56
- const merged = { ...loadFindFeaturesState(productId), ...patch };
57
- saveFindFeaturesState(productId, merged);
58
- return merged;
59
- }
60
- export function acquireFindFeaturesLock(productId, staleAfterMs = 60 * 60 * 1000) {
61
- ensureStateDir();
62
- const lock = lockPath(productId);
63
- if (existsSync(lock)) {
64
- try {
65
- const raw = readFileSync(lock, 'utf-8');
66
- const parsed = JSON.parse(raw);
67
- const age = Date.now() - new Date(parsed.acquiredAt ?? 0).getTime();
68
- if (age < staleAfterMs) {
69
- return null;
70
- }
71
- rmSync(lock, { force: true });
72
- }
73
- catch {
74
- rmSync(lock, { force: true });
75
- }
76
- }
77
- try {
78
- const fd = openSync(lock, 'wx');
79
- writeFileSync(fd, JSON.stringify({ acquiredAt: new Date().toISOString(), pid: process.pid }));
80
- return {
81
- release: () => {
82
- try {
83
- unlinkSync(lock);
84
- }
85
- catch {
86
- // already gone
87
- }
88
- },
89
- };
90
- }
91
- catch {
92
- return null;
93
- }
94
- }
15
+ import { createScanStateModule, } from '../find-shared/scan-state.js';
16
+ const m = createScanStateModule({
17
+ dirName: 'find-features-state',
18
+ });
19
+ export const loadFindFeaturesState = m.load;
20
+ export const saveFindFeaturesState = m.save;
21
+ export const updateFindFeaturesState = m.update;
22
+ export const acquireFindFeaturesLock = m.acquireLock;
@@ -2,7 +2,7 @@
2
2
  * Types for the find-features phase.
3
3
  */
4
4
  export type OpportunityConfidence = 'high' | 'medium' | 'low';
5
- export type OpportunitySource = 'user_feedback' | 'competitor_gap' | 'market_trend' | 'adjacent_workflow' | 'other';
5
+ export type OpportunitySource = 'user_feedback' | 'competitor_gap' | 'market_trend' | 'adjacent_workflow' | 'code_analysis' | 'other';
6
6
  export interface FeatureOpportunity {
7
7
  /** Short, action-oriented title (e.g. "Add bulk CSV export for invoices"). */
8
8
  title: string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Git helpers shared by the find-* phases (find-bugs, find-features,
3
+ * find-smells). All three clone a product's repo, sync to a branch, and need
4
+ * the same low-level rev/diff queries — keep them here so behaviour stays in
5
+ * lock-step.
6
+ *
7
+ * Read-only on the working tree; safe to call against any cwd that is a git
8
+ * repo.
9
+ */
10
+ export declare function gitRevParse(repoPath: string, ref: string): string;
11
+ /**
12
+ * Resolve the remote's default branch (e.g. "main", "master", "trunk") via the
13
+ * symbolic ref `refs/remotes/origin/HEAD`. Falls back to "main" if the ref is
14
+ * missing — the caller will surface a sync error if that's wrong.
15
+ */
16
+ export declare function detectDefaultBranch(repoPath: string): string;
17
+ /**
18
+ * True iff `ancestor` is reachable from `descendant` (i.e. `descendant`
19
+ * contains all of `ancestor`'s history). Used by incremental scans to decide
20
+ * whether a previously-scanned commit is still part of HEAD's history before
21
+ * trusting a `git diff base..head`.
22
+ */
23
+ export declare function isAncestor(repoPath: string, ancestor: string, descendant: string): boolean;
24
+ export declare function listChangedPaths(repoPath: string, baseSha: string, headSha: string): string[];
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Git helpers shared by the find-* phases (find-bugs, find-features,
3
+ * find-smells). All three clone a product's repo, sync to a branch, and need
4
+ * the same low-level rev/diff queries — keep them here so behaviour stays in
5
+ * lock-step.
6
+ *
7
+ * Read-only on the working tree; safe to call against any cwd that is a git
8
+ * repo.
9
+ */
10
+ import { execFileSync } from 'child_process';
11
+ export function gitRevParse(repoPath, ref) {
12
+ return execFileSync('git', ['rev-parse', ref], {
13
+ cwd: repoPath,
14
+ encoding: 'utf-8',
15
+ }).trim();
16
+ }
17
+ /**
18
+ * Resolve the remote's default branch (e.g. "main", "master", "trunk") via the
19
+ * symbolic ref `refs/remotes/origin/HEAD`. Falls back to "main" if the ref is
20
+ * missing — the caller will surface a sync error if that's wrong.
21
+ */
22
+ export function detectDefaultBranch(repoPath) {
23
+ try {
24
+ const ref = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: repoPath, encoding: 'utf-8' }).trim();
25
+ return ref.replace(/^origin\//, '');
26
+ }
27
+ catch {
28
+ return 'main';
29
+ }
30
+ }
31
+ /**
32
+ * True iff `ancestor` is reachable from `descendant` (i.e. `descendant`
33
+ * contains all of `ancestor`'s history). Used by incremental scans to decide
34
+ * whether a previously-scanned commit is still part of HEAD's history before
35
+ * trusting a `git diff base..head`.
36
+ */
37
+ export function isAncestor(repoPath, ancestor, descendant) {
38
+ try {
39
+ execFileSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
40
+ cwd: repoPath,
41
+ stdio: 'pipe',
42
+ });
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ export function listChangedPaths(repoPath, baseSha, headSha) {
50
+ try {
51
+ const out = execFileSync('git', ['diff', '--name-only', `${baseSha}..${headSha}`], { cwd: repoPath, encoding: 'utf-8' });
52
+ return out
53
+ .split('\n')
54
+ .map((s) => s.trim())
55
+ .filter((s) => s.length > 0);
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MCP helpers shared by find-bugs / find-features / find-smells.
3
+ *
4
+ * All three load a product's basics (name, description) for prompt context,
5
+ * pull the open-issue list for dedup, and file findings via `issues/create`.
6
+ * Centralising the calls means a schema change in MCP only needs touching one
7
+ * place, and the per-phase orchestrators stay focused on their own logic.
8
+ */
9
+ import { type IssueInfo } from '../../types/issues.js';
10
+ export interface ProductBasics {
11
+ name: string;
12
+ description?: string;
13
+ }
14
+ export declare function fetchProductBasics(productId: string): Promise<ProductBasics>;
15
+ /**
16
+ * Fetch the product's open issues for dedup context. Filters out terminal
17
+ * statuses (shipped/archived/closed/...) since those can't conflict with new
18
+ * findings the agent might surface.
19
+ */
20
+ export declare function fetchOpenIssues(productId: string): Promise<IssueInfo[]>;
21
+ export declare function isTerminalStatus(status: string): boolean;
22
+ export interface CreateIssueInput {
23
+ productId: string;
24
+ /** Issue title for `name`. */
25
+ title: string;
26
+ /** Already-formatted markdown body. The caller decides the layout. */
27
+ description: string;
28
+ }
29
+ /**
30
+ * File a new issue via MCP. Returns the new issue id, or null if MCP returned
31
+ * an error / unexpected shape (already logged).
32
+ */
33
+ export declare function createIssue(input: CreateIssueInput): Promise<string | null>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MCP helpers shared by find-bugs / find-features / find-smells.
3
+ *
4
+ * All three load a product's basics (name, description) for prompt context,
5
+ * pull the open-issue list for dedup, and file findings via `issues/create`.
6
+ * Centralising the calls means a schema change in MCP only needs touching one
7
+ * place, and the per-phase orchestrators stay focused on their own logic.
8
+ */
9
+ import { callMcpEndpoint } from '../../api/mcp-client.js';
10
+ import { logError, logWarning } from '../../utils/logger.js';
11
+ export async function fetchProductBasics(productId) {
12
+ try {
13
+ const result = (await callMcpEndpoint('resources/read', {
14
+ uri: `product://${productId}`,
15
+ }));
16
+ const text = result.contents?.[0]?.text || '{}';
17
+ const parsed = JSON.parse(text);
18
+ return {
19
+ name: parsed.name || productId,
20
+ description: parsed.description,
21
+ };
22
+ }
23
+ catch {
24
+ return { name: productId };
25
+ }
26
+ }
27
+ /**
28
+ * Fetch the product's open issues for dedup context. Filters out terminal
29
+ * statuses (shipped/archived/closed/...) since those can't conflict with new
30
+ * findings the agent might surface.
31
+ */
32
+ export async function fetchOpenIssues(productId) {
33
+ try {
34
+ const result = (await callMcpEndpoint('issues/list', {
35
+ product_id: productId,
36
+ }));
37
+ const all = result.issues || [];
38
+ return all.filter((i) => !isTerminalStatus(i.status));
39
+ }
40
+ catch (error) {
41
+ logWarning(`Could not load existing issues for dedup: ${error instanceof Error ? error.message : String(error)}`);
42
+ return [];
43
+ }
44
+ }
45
+ export function isTerminalStatus(status) {
46
+ return (status === 'shipped' ||
47
+ status === 'archived' ||
48
+ status === 'cancelled' ||
49
+ status === 'closed' ||
50
+ status === 'completed');
51
+ }
52
+ /**
53
+ * File a new issue via MCP. Returns the new issue id, or null if MCP returned
54
+ * an error / unexpected shape (already logged).
55
+ */
56
+ export async function createIssue(input) {
57
+ try {
58
+ const result = (await callMcpEndpoint('issues/create', {
59
+ product_id: input.productId,
60
+ name: input.title,
61
+ description: input.description,
62
+ }));
63
+ return result.issue?.id || result.id || null;
64
+ }
65
+ catch (error) {
66
+ logError(`Failed to create issue for "${input.title}": ${error instanceof Error ? error.message : String(error)}`);
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-product scan state, generic across the find-* phases. Each phase has
3
+ * its own state schema (find-bugs tracks a commit sha, find-features tracks
4
+ * the most-recent-seen feedback id, etc.) so the type is parametric, but the
5
+ * file layout, atomic-write, and lock semantics are identical and live here.
6
+ *
7
+ * Storage layout: `~/.edsger/<dirName>/<productId>.json` plus a sibling
8
+ * `.lock` file. The lockfile uses O_EXCL so concurrent acquirers race on the
9
+ * filesystem, not on application code — atomic on local POSIX filesystems
10
+ * (good enough; the workspace clone is also machine-local).
11
+ *
12
+ * Known limitation: state is machine-local. A scan started on machine A and
13
+ * next on machine B will look like a first run.
14
+ */
15
+ export interface LockHandle {
16
+ release: () => void;
17
+ }
18
+ export interface ScanStateModule<T extends object> {
19
+ load: (productId: string) => T;
20
+ save: (productId: string, state: T) => void;
21
+ /** Merge `patch` into the stored state. Fields not mentioned are preserved. */
22
+ update: (productId: string, patch: Partial<T>) => T;
23
+ /**
24
+ * Acquire an exclusive lock for this product's state. Returns null if held.
25
+ * Stale locks older than `staleAfterMs` (default 1h) are reclaimed.
26
+ */
27
+ acquireLock: (productId: string, staleAfterMs?: number) => LockHandle | null;
28
+ }
29
+ export interface CreateScanStateModuleOptions {
30
+ /** Subdirectory under ~/.edsger/. Example: 'find-bugs-state'. */
31
+ dirName: string;
32
+ }
33
+ export declare function createScanStateModule<T extends object>(opts: CreateScanStateModuleOptions): ScanStateModule<T>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Per-product scan state, generic across the find-* phases. Each phase has
3
+ * its own state schema (find-bugs tracks a commit sha, find-features tracks
4
+ * the most-recent-seen feedback id, etc.) so the type is parametric, but the
5
+ * file layout, atomic-write, and lock semantics are identical and live here.
6
+ *
7
+ * Storage layout: `~/.edsger/<dirName>/<productId>.json` plus a sibling
8
+ * `.lock` file. The lockfile uses O_EXCL so concurrent acquirers race on the
9
+ * filesystem, not on application code — atomic on local POSIX filesystems
10
+ * (good enough; the workspace clone is also machine-local).
11
+ *
12
+ * Known limitation: state is machine-local. A scan started on machine A and
13
+ * next on machine B will look like a first run.
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'fs';
16
+ import { homedir } from 'os';
17
+ import { join } from 'path';
18
+ const DEFAULT_STALE_AFTER_MS = 60 * 60 * 1000;
19
+ export function createScanStateModule(opts) {
20
+ const { dirName } = opts;
21
+ // Resolve at call time, not module-load time, so test setups that swap
22
+ // process.env.HOME between cases see the change.
23
+ function stateDir() {
24
+ return join(homedir(), '.edsger', dirName);
25
+ }
26
+ function safeProductId(productId) {
27
+ return productId.replace(/[^a-zA-Z0-9_-]/g, '_');
28
+ }
29
+ function statePath(productId) {
30
+ return join(stateDir(), `${safeProductId(productId)}.json`);
31
+ }
32
+ function lockPath(productId) {
33
+ return join(stateDir(), `${safeProductId(productId)}.lock`);
34
+ }
35
+ function ensureStateDir() {
36
+ const dir = stateDir();
37
+ if (!existsSync(dir)) {
38
+ mkdirSync(dir, { recursive: true });
39
+ }
40
+ }
41
+ function load(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
+ /** Atomic write: stage to a tempfile, then rename. */
56
+ function save(productId, state) {
57
+ ensureStateDir();
58
+ const finalPath = statePath(productId);
59
+ const tmpPath = `${finalPath}.${process.pid}.tmp`;
60
+ writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
61
+ renameSync(tmpPath, finalPath);
62
+ }
63
+ function update(productId, patch) {
64
+ const merged = { ...load(productId), ...patch };
65
+ save(productId, merged);
66
+ return merged;
67
+ }
68
+ function acquireLock(productId, staleAfterMs = DEFAULT_STALE_AFTER_MS) {
69
+ ensureStateDir();
70
+ const lock = lockPath(productId);
71
+ if (existsSync(lock)) {
72
+ try {
73
+ const raw = readFileSync(lock, 'utf-8');
74
+ const parsed = JSON.parse(raw);
75
+ const age = Date.now() - new Date(parsed.acquiredAt ?? 0).getTime();
76
+ if (age < staleAfterMs) {
77
+ return null;
78
+ }
79
+ // Stale or corrupt — reclaim. Only reachable if a previous run died
80
+ // without releasing (SIGKILL, OOM).
81
+ rmSync(lock, { force: true });
82
+ }
83
+ catch {
84
+ rmSync(lock, { force: true });
85
+ }
86
+ }
87
+ try {
88
+ // `flag: 'wx'` = open with O_CREAT | O_EXCL — atomic against a racing
89
+ // peer, and writeFileSync(path, ...) closes the fd internally so no
90
+ // manual closeSync is needed (vs. the openSync + writeFileSync(fd)
91
+ // form, which leaks the fd).
92
+ writeFileSync(lock, JSON.stringify({
93
+ acquiredAt: new Date().toISOString(),
94
+ pid: process.pid,
95
+ }), { flag: 'wx' });
96
+ return {
97
+ release: () => {
98
+ try {
99
+ unlinkSync(lock);
100
+ }
101
+ catch {
102
+ // Already gone — fine.
103
+ }
104
+ },
105
+ };
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ return { load, save, update, acquireLock };
112
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Find-smells phase: clone the product's repo, ask Claude to audit code for
3
+ * code smells (refactor candidates, perf cliffs, dead code, etc.), and file
4
+ * each new finding as an issue. Subsequent runs are incremental, scoped to
5
+ * commits since the previous successful scan.
6
+ *
7
+ * Companion to find-bugs (real defects) and find-features (missing user
8
+ * capability). The three phases share the same workspace + state pattern.
9
+ */
10
+ import { type SmellCategory } from './types.js';
11
+ export interface FindSmellsOptions {
12
+ productId: string;
13
+ githubToken: string;
14
+ owner: string;
15
+ repo: string;
16
+ /** Force a full scan even if previous-scan state exists. */
17
+ full?: boolean;
18
+ /** Branch to scan; defaults to the repo's default branch. */
19
+ branch?: string;
20
+ /** Upper bound on files the auditor may Read. Keeps token cost predictable. */
21
+ maxFiles?: number;
22
+ /**
23
+ * Optional category filter. Agent is asked to honour it via the prompt
24
+ * AND its output is re-filtered after parsing — both are required because
25
+ * the agent can't be trusted to obey perfectly.
26
+ */
27
+ categories?: SmellCategory[];
28
+ verbose?: boolean;
29
+ }
30
+ export interface FindSmellsResult {
31
+ status: 'success' | 'error';
32
+ message: string;
33
+ scannedCommitSha?: string;
34
+ smellsFound?: number;
35
+ issuesCreated?: number;
36
+ /** Findings the agent passed off to find-bugs (not filed by this run). */
37
+ deferredToBugs?: number;
38
+ /** Findings the agent passed off to find-features (not filed by this run). */
39
+ deferredToFeatures?: number;
40
+ summary?: string;
41
+ }
42
+ /** Single source of truth — referenced by the CLI help string too. */
43
+ export declare const DEFAULT_MAX_FILES = 200;
44
+ /**
45
+ * Scan a product's repository for code smells and file each new finding as an issue.
46
+ */
47
+ export declare function scanForSmells(options: FindSmellsOptions): Promise<FindSmellsResult>;