feat: AI-powered inter-tag cluster generation#1632
Conversation
🦋 Changeset detectedLatest commit: 3acc113 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughAdds AI-powered inter-tag cluster generation: new CLI command, interactive editor/renderer, AI bridge and prompt builder, semantic analysis + dependency synthesizer, caching, core ClusterGenerationService, tests, and tm-core public exports. Changes
Sequence DiagramsequenceDiagram
actor User
participant CLI as "CLI Command"
participant Core as "TmCore"
participant Service as "ClusterGenerationService"
participant Cache as "TagAnalysisCache"
participant Analyzer as "TagSemanticAnalyzer"
participant Synth as "TagDependencySynthesizer"
participant Editor as "ClusterEditor"
participant Storage as "Cache Storage"
User->>CLI: tm clusters generate [--auto] [--json]
CLI->>Core: initialize & fetch tags
CLI->>Service: generate(tags, onProgress)
Service->>Cache: load cached entries
Cache->>Storage: load()
Storage-->>Cache: cache file
Service->>Analyzer: analyze uncached tag (concurrent batches)
Analyzer-->>Service: SemanticAnalysis
Service->>Cache: set(new analyses)
Cache->>Storage: save(updated)
Service->>Synth: synthesize(all analyses)
Synth-->>Service: DependencySuggestion[]
Service->>Service: compute clusters & reasoning
Service-->>CLI: ClusterSuggestion
alt --json
CLI-->>User: JSON output
else --auto
CLI->>Core: persistClusterDependencies()
Core-->>CLI: persisted
CLI-->>User: rendered layout
else interactive
CLI->>Editor: editClusters(clusters, dependencies)
Editor-->>User: display & accept/move/reset/cancel
Editor-->>CLI: ClusterEditorResult
alt accepted
CLI->>Core: persistClusterDependencies()
Core-->>CLI: persisted
CLI-->>User: updated layout
else cancelled
CLI-->>User: aborted
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/cli/src/commands/cluster-generate.command.ts`:
- Around line 104-136: The rollback in persistClusterDependencies currently
rethrows the original add error but ignores failures that occur while restoring
snapshot dependencies; update the catch block in persistClusterDependencies to
catch and surface restore failures: when catching the initial error from
tmCore.tasks.addTagDependency, attempt to restore each snapshot entry but wrap
each restore call (tmCore.tasks.addTagDependency and
tmCore.tasks.removeTagDependency where applicable) in its own try/catch, collect
any restore errors (including which tagName/dep failed) and log them via the
same logger or attach them to a new AggregateError, then throw a combined error
that includes both the original add error and any restore errors so callers know
the full failure set.
🧹 Nitpick comments (5)
.changeset/ai-cluster-generation.md (1)
1-6: Expand the changeset with user-facing detail + examples.
This is a major new CLI command; the current single-line summary undersells the feature and lacks usage examples. Consider adding a short “Highlights” list plus a few command examples.✍️ Suggested changeset expansion
Add `tm clusters generate` command that uses AI to analyze tags in parallel, suggest inter-tag dependencies, and present an interactive terminal UI to review and re-order the cluster layout before persisting. Supports `--auto` for non-interactive mode and `--json` for machine-readable output. + +Highlights: +- AI analyzes tag semantics and proposes inter-tag dependencies with reasoning. +- Interactive TUI to edit, reorder, accept/reject dependencies before saving. +- Cache prevents redundant AI calls and speeds up repeat runs. + +Examples: +- `tm clusters generate` +- `tm clusters generate --auto` +- `tm clusters generate --json`Based on learnings: For major feature additions like new CLI commands, eyaltoledano prefers detailed changesets with comprehensive descriptions, usage examples, and feature explanations rather than minimal single-line summaries.
packages/tm-core/src/modules/ai/legacy-ai-loader.ts (1)
23-24: Well-implemented temporary bridge—add path resolution test.The hardcoded 5-level relative path correctly resolves to the target file and is properly documented as a temporary measure. The implementation correctly uses
import.meta.urlandpath.resolve()for ESM path resolution, includes awebpackIgnorecomment, and validates that the imported function exists with comprehensive error handling.Add a test verifying the path resolution at runtime to catch breakage early if the module structure changes.
packages/tm-core/src/modules/cluster/generation/tag-analysis-cache.ts (1)
40-54: Potential race condition in concurrent cache writes.The
setmethod uses a read-modify-write pattern (load→ merge →save) without locking. If multiple analyses complete concurrently, one write could overwrite another's entry. In practice, the current batched analysis flow inClusterGenerationService(withMAX_CONCURRENCY = 5) means multiple tags could finish simultaneously.Consider either:
- Making cache writes sequential (queue them)
- Using atomic file operations with read-modify-write in storage
- Accepting this as a minor edge case (cache miss just triggers re-analysis)
Given that a cache miss only costs an extra AI call rather than data loss, this is acceptable for now but worth noting.
packages/tm-core/src/modules/cluster/generation/cluster-generation.service.ts (1)
69-135: LGTM with minor optimization opportunity.The batched analysis flow with
MAX_CONCURRENCY = 5is well-designed. Cache hits are correctly identified upfront, and progress reporting provides good UX.One minor optimization: the cache lookup loop (lines 78-91) is sequential. For many tags, parallel lookups could be faster:
const lookupResults = await Promise.all( tags.map(async (tag) => { const hash = TagAnalysisCache.computeHash(tag); const hit = this.cache ? await this.cache.get(tag.name, hash) : null; return { tag, hash, hit }; }) );This is optional since cache I/O is typically fast.
apps/cli/src/commands/cluster-generate.command.ts (1)
138-155: Cache file parsing lacks validation.
JSON.parse(raw) as CacheFile(line 145) trusts that the file content matches the expected schema. A corrupted or manually-edited cache file could cause runtime errors downstream.Consider adding basic validation or using a try-catch with a fallback to
null:🛡️ Suggested defensive parsing
load: async (): Promise<CacheFile | null> => { try { const raw = await fs.readFile(cachePath, 'utf-8'); - return JSON.parse(raw) as CacheFile; + const parsed = JSON.parse(raw); + // Basic shape validation + if (parsed?.version === 1 && typeof parsed?.entries === 'object') { + return parsed as CacheFile; + } + return null; } catch { return null; } },
| export async function persistClusterDependencies( | ||
| tmCore: TmCore, | ||
| allTagNames: readonly string[], | ||
| dependencies: readonly DependencySuggestion[] | ||
| ): Promise<void> { | ||
| // Snapshot existing dependencies before removal so we can restore on failure | ||
| const snapshot = new Map<string, readonly string[]>(); | ||
| for (const tagName of allTagNames) { | ||
| const existingDeps = await tmCore.tasks.getTagDependencies(tagName); | ||
| snapshot.set(tagName, existingDeps); | ||
| } | ||
|
|
||
| // Remove all existing deps, then add new ones | ||
| for (const tagName of allTagNames) { | ||
| for (const dep of snapshot.get(tagName) ?? []) { | ||
| await tmCore.tasks.removeTagDependency(tagName, dep); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| for (const dep of dependencies) { | ||
| await tmCore.tasks.addTagDependency(dep.from, dep.to); | ||
| } | ||
| } catch (error) { | ||
| // Restore original dependencies from snapshot | ||
| for (const [tagName, deps] of snapshot) { | ||
| for (const dep of deps) { | ||
| await tmCore.tasks.addTagDependency(tagName, dep); | ||
| } | ||
| } | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Restore failure in rollback could leave inconsistent state.
The rollback logic (lines 129-133) attempts to restore dependencies if adding new ones fails, but if the restore itself fails, the error is swallowed and the original add error is re-thrown. This could leave the system in an inconsistent state (some deps removed, new ones partially added, restore failed).
Consider logging restore failures or wrapping the entire operation differently:
🛡️ Suggested improvement
} catch (error) {
// Restore original dependencies from snapshot
+ const restoreErrors: Error[] = [];
for (const [tagName, deps] of snapshot) {
for (const dep of deps) {
- await tmCore.tasks.addTagDependency(tagName, dep);
+ try {
+ await tmCore.tasks.addTagDependency(tagName, dep);
+ } catch (restoreError) {
+ restoreErrors.push(restoreError as Error);
+ }
}
}
+ if (restoreErrors.length > 0) {
+ console.error(chalk.red(`Warning: Failed to restore ${restoreErrors.length} dependencies during rollback`));
+ }
throw error;
}🤖 Prompt for AI Agents
In `@apps/cli/src/commands/cluster-generate.command.ts` around lines 104 - 136,
The rollback in persistClusterDependencies currently rethrows the original add
error but ignores failures that occur while restoring snapshot dependencies;
update the catch block in persistClusterDependencies to catch and surface
restore failures: when catching the initial error from
tmCore.tasks.addTagDependency, attempt to restore each snapshot entry but wrap
each restore call (tmCore.tasks.addTagDependency and
tmCore.tasks.removeTagDependency where applicable) in its own try/catch, collect
any restore errors (including which tagName/dep failed) and log them via the
same logger or attach them to a new AggregateError, then throw a combined error
that includes both the original add error and any restore errors so callers know
the full failure set.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/cli/src/commands/cluster-generate.command.ts`:
- Around line 118-137: The rollback currently only restores the original
snapshot and doesn't remove any partially-added new dependencies, so if some
tmCore.tasks.addTagDependency calls succeed before a failure you end up with
mixed dependencies; modify the add loop to track successful additions (e.g.,
push {from, to} into a succeededAdds array when
tmCore.tasks.addTagDependency(dep.from, dep.to) resolves), and in the catch
block first remove all succeededAdds via tmCore.tasks.removeTagDependency(from,
to) before restoring the original snapshot, and ensure any errors during restore
are propagated (don't swallow them) by rethrowing the original error or
aggregating errors as needed; reference snapshot, dependencies, allTagNames,
tmCore.tasks.addTagDependency, and tmCore.tasks.removeTagDependency when
applying the fix.
In `@apps/cli/src/ui/components/cluster-editor.component.ts`:
- Around line 100-146: The loop uses direct console calls (console.clear,
console.log) which must be replaced with the project's central logging utility;
update the block that renders renderTagClusterLayout(state.clusters,
state.dependencies, reasoning) and subsequent blank line to call the central
log/info method instead, replace console.clear() with the logging utility's
terminal-clear or equivalent method, and change the warning print inside the
'move' branch (console.log(chalk.yellow(...))) to the logging utility's
warning/error method; keep existing behavior and strings, and reference the same
symbols: renderTagClusterLayout, state, reasoning, and the inquirer.prompt flow.
In
`@packages/tm-core/src/modules/ai/structured-generation/structured-generator.ts`:
- Around line 42-67: In generate
(packages/tm-core/src/modules/ai/structured-generation/structured-generator.ts)
you currently cast result.mainResult to T without validating it; add a guard
after the await this.generateObjectService(...) call to check that result and
result.mainResult are present and non-undefined, and if not throw a clear, early
error (include context such as objectName/commandName/modelId/providerName) so
the failure is explicit instead of returning undefined; ensure the validated
value is then used for data: (not cast blindly) and preserve the existing
telemetry and fallback model/provider behavior.
🧹 Nitpick comments (3)
packages/tm-core/src/modules/cluster/generation/tag-dependency-synthesizer.ts (1)
41-55: Skip AI calls when there’s nothing to analyze.If
analysesis empty, we can return immediately and avoid an unnecessary AI call.♻️ Proposed refactor
async synthesize( analyses: ReadonlyArray<{ label: string; analysis: SemanticAnalysis }>, options: AIPrimitiveOptions ): Promise<AIPrimitiveResult<readonly DependencySuggestion[]>> { - const startTime = Date.now(); + if (analyses.length === 0) { + return { + data: [], + usage: { + inputTokens: 0, + outputTokens: 0, + model: 'unknown', + provider: 'unknown', + duration: 0 + } + }; + } + + const startTime = Date.now();packages/tm-core/src/modules/cluster/generation/cluster-generation.service.ts (1)
125-128: Cache write is fire-and-forget; failures are silently ignored.If
this.cache.set()throws, the error propagates and fails the entire batch. Consider wrapping the cache write in a try-catch to make caching best-effort, preventing cache failures from breaking analysis:🛡️ Optional defensive improvement
if (this.cache) { const hash = hashByTag.get(tag.name)!; - await this.cache.set(tag.name, hash, analysis); + try { + await this.cache.set(tag.name, hash, analysis); + } catch { + // Cache write failure is non-fatal; analysis proceeds + } }apps/cli/src/commands/cluster-generate.command.ts (1)
148-156: Consider usingreadJSONutility for JSON file operations.Per coding guidelines, JSON file operations should use
readJSON/writeJSONutilities with proper error handling and validation. The current implementation has no schema validation for the parsed cache file.🛡️ Suggested improvement for robustness
load: async (): Promise<CacheFile | null> => { try { const raw = await fs.readFile(cachePath, 'utf-8'); - return JSON.parse(raw) as CacheFile; + const parsed = JSON.parse(raw); + // Basic structural validation + if (parsed?.version !== 1 || typeof parsed?.entries !== 'object') { + return null; + } + return parsed as CacheFile; } catch { return null; } },As per coding guidelines: "Include error handling for JSON file operations and validate JSON structure after reading."
packages/tm-core/src/modules/ai/structured-generation/structured-generator.ts
Show resolved
Hide resolved
Keep both ClusterExecutionDomain (from next) and ClusterGenerationService (from feature branch). Restore legacy-ai-loader.ts and its export lost during merge. Register both start and generate subcommands.
…ependencies Extract reusable `runClusterGeneration()` and `persistClusterDependencies()` helpers from ClusterGenerateCommand so both `tm clusters generate` and the new inline prompt in `tm clusters` can share the same generation logic. When `tm clusters` detects zero inter-tag dependencies in TTY mode, it now prompts "Generate cluster ordering with AI?" instead of just printing a hint. Non-interactive modes (--json, --tree, --diagram, piped) bypass the prompt. Adds --auto flag to skip the prompt and auto-generate.
Move generateObjectService loading from fragile relative import path into @tm/core via loadGenerateObjectService(). Add warning log when --auto mode replaces existing inter-tag dependencies.
7d8dbcd to
8f05526
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/cli/src/commands/clusters.command.ts (1)
206-317:⚠️ Potential issue | 🟡 MinorRoute new console output through the logging utility.
The added inline‑generation flow introduces multipleconsole.logcalls; please switch these to the central logger so silent mode and log routing are consistent.As per coding guidelines, "Do not add direct console.log calls outside the logging utility - use the central log function instead."
🤖 Fix all issues with AI agents
In `@apps/cli/src/commands/clusters.command.ts`:
- Around line 194-223: The --auto flag is currently only honoured inside the
isInteractive branch, so CI/non‑TTY runs never trigger generation; modify the
logic around options.auto and isInteractive in the block that checks
allIndependent so that options.auto can trigger runInlineGenerate even when
process.stdin.isTTY is false. Specifically, when detection.clusters indicates
all independent and tagsResult.tags.length >= 2, allow either options.auto OR
(isInteractive && await this.promptForGeneration()) to decide generation; call
this.runInlineGenerate(options, tagsResult.tags) when that combined condition is
true (keep the existing console messages for the non‑generation path).
🧹 Nitpick comments (2)
apps/cli/src/ui/components/cluster-editor.component.ts (1)
54-77: Cartesian product may generate excessive dependencies.The
recalculateDependenciesfunction creates a dependency from every tag in level N to every tag in level N-1. For levels with many tags, this could produce an unexpectedly large number of dependencies (e.g., 5 tags × 5 tags = 25 dependencies per level transition).Consider whether this is the intended behavior or if a simpler "level depends on previous level" relationship would suffice.
packages/tm-core/src/modules/cluster/generation/cluster-generation.service.ts (1)
100-135: Consider error handling for individual tag analysis failures.If
this.analyzer.analyze()throws for one tag in a batch,Promise.allwill reject the entire batch, losing partial progress. Depending on desired behavior, you may want to usePromise.allSettledand handle individual failures gracefully, or at minimum wrap with try/catch to provide better error context.💡 Optional: Use allSettled for partial failure tolerance
- const batchResults = await Promise.all( - batch.map(async (tag, batchIndex) => { + const batchSettled = await Promise.allSettled( + batch.map(async (tag, batchIndex) => { // ... existing logic ... }) ); - - results.push(...batchResults); + + for (const result of batchSettled) { + if (result.status === 'fulfilled') { + results.push(result.value); + } else { + // Log or handle the failure for this tag + throw new Error(`Analysis failed: ${result.reason}`); + } + }
8f05526 to
aa77bd5
Compare
Track successful dependency additions during persist so rollback removes partial adds before restoring the snapshot. Add validation guard in BridgedStructuredGenerator to throw descriptive errors when AI service returns null/undefined instead of failing silently.
aa77bd5 to
3acc113
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| { | ||
| "setup": [], | ||
| "teardown": [] | ||
| } |
There was a problem hiding this comment.
Unreferenced .superset/config.json committed to repository
Low Severity
The .superset/config.json file is added in this commit but is not referenced anywhere in the codebase — no imports, no code reads from this path, and no documentation mentions it. This appears to be an unrelated config file that was accidentally staged and committed alongside the cluster generation feature.
|
|
||
| if (shouldGenerate) { | ||
| await this.runInlineGenerate(options, tagsResult.tags); | ||
| return; |
There was a problem hiding this comment.
Inline generation ignores --json flag when --auto is set
Medium Severity
In renderTagClusters, the --json early return at line 181 only fires when there ARE existing dependencies. When all tags are independent, options.json is checked at line 181 but renderTagJson returns the empty detection. The shouldGenerate check at line 208 evaluates options.auto first, bypassing the isInteractive guard that excludes --json. Inside runInlineGenerate, the --auto path always renders visual output, unlike ClusterGenerateCommand which correctly checks --json before --auto.
Additional Locations (1)
| if (this.cache) { | ||
| const hash = hashByTag.get(tag.name)!; | ||
| await this.cache.set(tag.name, hash, analysis); | ||
| } |
There was a problem hiding this comment.
Concurrent cache writes cause silent entry loss
Medium Severity
Within each batch of up to MAX_CONCURRENCY (5) concurrent AI calls, every completed analysis writes to the cache via cache.set(). The set method performs a non-atomic read-modify-write: it loads the full file, merges one entry, and saves the full file. When multiple set calls from the same batch overlap, later writes overwrite earlier ones, silently discarding cached entries. This defeats the purpose of the caching layer — subsequent runs re-analyze tags that were already successfully analyzed, wasting AI API calls and time.


Summary
tm clusters generate): Analyzes tags semantically with AI, synthesizes inter-tag dependencies, and suggests a topological execution order with reasoningtm clustersdetects no inter-tag dependencies, it offers to run AI generation inlineTagAnalysisCacheavoids redundant AI calls for previously analyzed tagsloadGenerateObjectService()in@tm/corereplaces fragile relative import paths--autoflag now warns when replacing existing inter-tag dependenciesArchitecture
Follows domain-driven design in
@tm/core:cluster/generation/—ClusterGenerationService, semantic analyzer, dependency synthesizer, cacheai/—PromptBuilder,BridgedStructuredGenerator,loadGenerateObjectService(legacy bridge)@tm/corefacadesNew files (32 changed, +2311 lines)
cluster-generation.service.ts,tag-semantic-analyzer.ts,tag-dependency-synthesizer.tstag-analysis-cache.tslegacy-ai-loader.ts,PromptBuilder,BridgedStructuredGeneratorcluster-generate.command.ts,cluster-editor.component.ts,cluster-layout-renderer.tscluster-generation.service.spec.ts,tag-analysis-cache.spec.tsTest plan
tm clusters generatewith 2+ tags — verify AI analysis and interactive editortm clusters generate --autowith existing deps — verify yellow warning appearstm clusters generate --json— verify JSON outputtm clusters generate --no-cache— verify fresh analysistm clusterswith no deps — verify auto-prompt to generatenpm run test -w @tm/corenpm run turbo:typecheck🤖 Generated with Claude Code
Note
Medium Risk
Adds AI-driven dependency generation and new write-paths that can replace existing tag dependencies; correctness depends on AI output and rollback logic during persistence.
Overview
Adds AI-powered inter-tag cluster generation: new
tm clusters generateanalyzes tag/task content, synthesizesdependsOnsuggestions, and can persist them either via--autoor an interactive in-terminal editor (with non-interactive--jsonoutput).Enhances
tm clustersso that when no inter-tag dependencies exist it can prompt (or auto-run via--auto) the same generation flow inline, then re-renders clusters after saving.Introduces
@tm/coreinfrastructure and domain code to support this:cluster/generationpipeline (ClusterGenerationService, semantic analyzer, dependency synthesizer) with an analysis cache (TagAnalysisCachepersisted under.taskmaster/cache/cluster-analysis.json), plus a centralizedloadGenerateObjectService()bridge for the legacy AI module and supporting prompt/structured-generation utilities and tests.Updates repo metadata/docs: adds a changeset for a major release, documents module-organization rules in
CLAUDE.md, and extends.taskmaster/tasks/tasks.jsontag metadata withdependsOnrelationships.Written by Cursor Bugbot for commit 3acc113. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation