Skip to content

Commit dacc6e2

Browse files
jpicklykclaude
andauthored
feat: ktlint enforcement and short UUID prefix lookup fix (#73)
* feat: add ktlint linting with Gradle integration, .editorconfig, and CI enforcement Add ktlint (v12.1.2, engine v1.5.0) via jlleitschuh/ktlint-gradle plugin for automated Kotlin code style enforcement. Includes .editorconfig with wildcard import allowlist, disabled trailing comma rules, and 140-char line limit. CI workflow gets a ktlintCheck step before tests. Applied ktlint formatting across ~108 source files (mechanical whitespace/blank line changes only). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use HEX() for short UUID prefix lookup in SQLite CAST(blob AS VARCHAR) produces raw bytes in SQLite, not a formatted UUID string, causing prefix lookups to always return empty results. Switch to LOWER(HEX(id)) for SQLite and LOWER(RAWTOHEX(id)) for H2, which both produce dashless lowercase hex strings suitable for LIKE prefix matching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: ktlint formatting for multiline expression in findByIdPrefix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 52c5b35 commit dacc6e2

File tree

110 files changed

+19561
-15806
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+19561
-15806
lines changed

.claude/settings.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
{
2-
"enabledPlugins": {
3-
"task-orchestrator@task-orchestrator-marketplace": true
4-
}
2+
"enabledPlugins": {}
53
}

.editorconfig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
indent_style = space
9+
indent_size = 4
10+
11+
[*.{kt,kts}]
12+
# Allow wildcard imports for heavily-used packages
13+
ij_kotlin_packages_to_use_import_on_demand = kotlinx.serialization.json.*,io.github.jpicklyk.mcptask.current.domain.model.*,io.github.jpicklyk.mcptask.current.application.tools.*,io.mockk.*,io.modelcontextprotocol.kotlin.sdk.types.*,kotlin.test.*,org.junit.jupiter.api.Assertions.*
14+
15+
# Disable trailing comma enforcement (codebase doesn't use them)
16+
ktlint_standard_trailing-comma-on-call-site = disabled
17+
ktlint_standard_trailing-comma-on-declaration-site = disabled
18+
19+
# Line length
20+
max_line_length = 140
21+
22+
[*.md]
23+
trim_trailing_whitespace = false

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
- name: Make gradlew executable
2929
run: chmod +x gradlew
3030

31+
- name: Lint check
32+
run: ./gradlew :current:ktlintCheck --no-daemon
33+
3134
- name: Run tests
3235
run: ./gradlew :current:test --no-daemon
3336

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
plugins {
66
alias(libs.plugins.kotlin.jvm) apply false
77
alias(libs.plugins.kotlin.serialization) apply false
8+
alias(libs.plugins.ktlint) apply false
89
}
910

1011
allprojects {

current/build.gradle.kts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import java.util.Properties
33
plugins {
44
alias(libs.plugins.kotlin.jvm)
55
alias(libs.plugins.kotlin.serialization)
6+
alias(libs.plugins.ktlint)
7+
}
8+
9+
ktlint {
10+
version.set("1.5.0")
11+
android.set(false)
12+
outputToConsole.set(true)
13+
ignoreFailures.set(false)
14+
filter {
15+
exclude("**/generated/**")
16+
}
617
}
718

819
// Load version from centralized version.properties
@@ -23,17 +34,18 @@ tasks.register("printTagVersion") {
2334
}
2435

2536
// Generate build-info resource so the app can read its version at runtime
26-
val generateBuildInfo = tasks.register("generateBuildInfo") {
27-
val outputDir = layout.buildDirectory.dir("generated/resources/build-info")
28-
val versionValue = version.toString()
29-
inputs.property("version", versionValue)
30-
outputs.dir(outputDir)
31-
doLast {
32-
val dir = outputDir.get().asFile.resolve("build-info")
33-
dir.mkdirs()
34-
dir.resolve("version.properties").writeText("version=$versionValue\n")
37+
val generateBuildInfo =
38+
tasks.register("generateBuildInfo") {
39+
val outputDir = layout.buildDirectory.dir("generated/resources/build-info")
40+
val versionValue = version.toString()
41+
inputs.property("version", versionValue)
42+
outputs.dir(outputDir)
43+
doLast {
44+
val dir = outputDir.get().asFile.resolve("build-info")
45+
dir.mkdirs()
46+
dir.resolve("version.properties").writeText("version=$versionValue\n")
47+
}
3548
}
36-
}
3749
sourceSets.main { resources.srcDir(generateBuildInfo) }
3850

3951
group = "io.github.jpicklyk"
@@ -105,7 +117,8 @@ tasks.jar {
105117

106118
dependsOn(configurations.runtimeClasspath)
107119
from({
108-
configurations.runtimeClasspath.get()
120+
configurations.runtimeClasspath
121+
.get()
109122
.filter { it.name.endsWith("jar") }
110123
.map { zipTree(it) }
111124
})

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/CurrentMain.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ fun main() {
2828
SignalHandler.install(coordinator)
2929

3030
// Register JVM shutdown hook as fallback
31-
Runtime.getRuntime().addShutdownHook(Thread {
32-
coordinator.initiateShutdown("JVM shutdown hook")
33-
coordinator.awaitCompletion(5000)
34-
})
31+
Runtime.getRuntime().addShutdownHook(
32+
Thread {
33+
coordinator.initiateShutdown("JVM shutdown hook")
34+
coordinator.awaitCompletion(5000)
35+
}
36+
)
3537

3638
// Create and run the MCP server (blocks until server closes)
3739
val mcpServer = CurrentMcpServer(version, coordinator)

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/CascadeDetector.kt

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ data class UnblockedItem(
4242
* whose incoming blocking dependencies are now fully satisfied.
4343
*/
4444
class CascadeDetector {
45-
4645
companion object {
4746
/** Maximum ancestor depth for recursive cascade detection. */
4847
const val MAX_DEPTH = 3
@@ -85,10 +84,11 @@ class CascadeDetector {
8584

8685
// Get role counts for all children of the parent
8786
val countsResult = workItemRepository.countChildrenByRole(parentId)
88-
val roleCounts = when (countsResult) {
89-
is Result.Success -> countsResult.data
90-
is Result.Error -> return emptyList()
91-
}
87+
val roleCounts =
88+
when (countsResult) {
89+
is Result.Success -> countsResult.data
90+
is Result.Error -> return emptyList()
91+
}
9292

9393
// If there are no children at all, no cascade
9494
if (roleCounts.isEmpty()) return emptyList()
@@ -99,26 +99,29 @@ class CascadeDetector {
9999

100100
// All children are terminal -- create cascade event for the parent
101101
val parentResult = workItemRepository.getById(parentId)
102-
val parent = when (parentResult) {
103-
is Result.Success -> parentResult.data
104-
is Result.Error -> return emptyList()
105-
}
102+
val parent =
103+
when (parentResult) {
104+
is Result.Success -> parentResult.data
105+
is Result.Error -> return emptyList()
106+
}
106107

107108
// If parent is already terminal, no cascade needed
108109
if (parent.role == Role.TERMINAL) return emptyList()
109110

110-
val event = CascadeEvent(
111-
itemId = parent.id,
112-
currentRole = parent.role,
113-
targetRole = Role.TERMINAL
114-
)
111+
val event =
112+
CascadeEvent(
113+
itemId = parent.id,
114+
currentRole = parent.role,
115+
targetRole = Role.TERMINAL
116+
)
115117

116118
// Recursively check the parent's parent
117-
val upstreamEvents = if (parent.parentId != null) {
118-
detectCascadesRecursive(parent.parentId, workItemRepository, depth + 1)
119-
} else {
120-
emptyList()
121-
}
119+
val upstreamEvents =
120+
if (parent.parentId != null) {
121+
detectCascadesRecursive(parent.parentId, workItemRepository, depth + 1)
122+
} else {
123+
emptyList()
124+
}
122125

123126
return listOf(event) + upstreamEvents
124127
}
@@ -146,19 +149,22 @@ class CascadeDetector {
146149

147150
// Fetch parent
148151
val parentResult = workItemRepository.getById(parentId)
149-
val parent = when (parentResult) {
150-
is Result.Success -> parentResult.data
151-
is Result.Error -> return emptyList()
152-
}
152+
val parent =
153+
when (parentResult) {
154+
is Result.Success -> parentResult.data
155+
is Result.Error -> return emptyList()
156+
}
153157

154158
// Parent must be in QUEUE to cascade
155159
if (parent.role != Role.QUEUE) return emptyList()
156160

157-
return listOf(CascadeEvent(
158-
itemId = parent.id,
159-
currentRole = parent.role,
160-
targetRole = Role.WORK
161-
))
161+
return listOf(
162+
CascadeEvent(
163+
itemId = parent.id,
164+
currentRole = parent.role,
165+
targetRole = Role.WORK
166+
)
167+
)
162168
}
163169

164170
// -----------------------------------------------------------------------
@@ -180,19 +186,22 @@ class CascadeDetector {
180186

181187
val parentId = item.parentId ?: return emptyList()
182188

183-
val parent = when (val result = workItemRepository.getById(parentId)) {
184-
is Result.Success -> result.data
185-
is Result.Error -> return emptyList()
186-
}
189+
val parent =
190+
when (val result = workItemRepository.getById(parentId)) {
191+
is Result.Success -> result.data
192+
is Result.Error -> return emptyList()
193+
}
187194

188195
// Only cascade if parent is TERMINAL
189196
if (parent.role != Role.TERMINAL) return emptyList()
190197

191-
return listOf(CascadeEvent(
192-
itemId = parent.id,
193-
currentRole = Role.TERMINAL,
194-
targetRole = Role.WORK
195-
))
198+
return listOf(
199+
CascadeEvent(
200+
itemId = parent.id,
201+
currentRole = Role.TERMINAL,
202+
targetRole = Role.WORK
203+
)
204+
)
196205
}
197206

198207
// -----------------------------------------------------------------------
@@ -230,10 +239,11 @@ class CascadeDetector {
230239
if (isFullyUnblocked(targetId, dependencyRepository, workItemRepository)) {
231240
// Fetch the target item to get its title
232241
val targetResult = workItemRepository.getById(targetId)
233-
val targetItem = when (targetResult) {
234-
is Result.Success -> targetResult.data
235-
is Result.Error -> continue
236-
}
242+
val targetItem =
243+
when (targetResult) {
244+
is Result.Success -> targetResult.data
245+
is Result.Error -> continue
246+
}
237247
unblockedItems.add(UnblockedItem(itemId = targetItem.id, title = targetItem.title))
238248
}
239249
}
@@ -260,10 +270,11 @@ class CascadeDetector {
260270

261271
// Get the blocker's current state
262272
val blockerResult = workItemRepository.getById(dep.fromItemId)
263-
val blockerItem = when (blockerResult) {
264-
is Result.Success -> blockerResult.data
265-
is Result.Error -> return false // Missing blocker counts as still blocked
266-
}
273+
val blockerItem =
274+
when (blockerResult) {
275+
is Result.Success -> blockerResult.data
276+
is Result.Error -> return false // Missing blocker counts as still blocked
277+
}
267278

268279
// If the blocker hasn't reached the threshold, this item is still blocked
269280
if (!Role.isAtOrBeyond(blockerItem.role, thresholdRole)) {

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/ItemHierarchyValidator.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import java.util.UUID
1515
* - Maximum depth enforcement
1616
*/
1717
class ItemHierarchyValidator {
18-
1918
companion object {
2019
/** Maximum allowed nesting depth for WorkItems. */
2120
const val MAX_DEPTH = 3
@@ -49,11 +48,12 @@ class ItemHierarchyValidator {
4948
val visited = mutableSetOf<UUID>()
5049
var cursor: UUID? = parentId
5150
while (cursor != null && visited.size <= MAX_DEPTH) {
52-
if (!visited.add(cursor)) break // Pre-existing cycle — stop walking
53-
val ancestor = when (val ancestorResult = repo.getById(cursor)) {
54-
is Result.Success -> ancestorResult.data
55-
is Result.Error -> break
56-
}
51+
if (!visited.add(cursor)) break // Pre-existing cycle — stop walking
52+
val ancestor =
53+
when (val ancestorResult = repo.getById(cursor)) {
54+
is Result.Success -> ancestorResult.data
55+
is Result.Error -> break
56+
}
5757
if (ancestor.id == itemId) {
5858
throw ToolValidationException(
5959
"$errorPrefix: reparenting to '$parentId' would create a circular hierarchy"

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/NoteSchemaService.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import io.github.jpicklyk.mcptask.current.domain.model.NoteSchemaEntry
1212
* return null / false, and transitions proceed without gate enforcement.
1313
*/
1414
interface NoteSchemaService {
15-
1615
/**
1716
* Returns the schema entries for the first tag in [tags] that matches a
1817
* declared schema, or null if no schema matches (schema-free mode).
@@ -24,8 +23,7 @@ interface NoteSchemaService {
2423
* Used to determine whether `start` from WORK should advance to REVIEW or jump to TERMINAL.
2524
* Returns false when no schema matches (schema-free mode — skip REVIEW).
2625
*/
27-
fun hasReviewPhase(tags: List<String>): Boolean =
28-
getSchemaForTags(tags)?.any { it.role == "review" } ?: false
26+
fun hasReviewPhase(tags: List<String>): Boolean = getSchemaForTags(tags)?.any { it.role == "review" } ?: false
2927
}
3028

3129
/**

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/PhaseNoteContext.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ fun computePhaseNoteContext(
4545

4646
val roleStr = role.name.lowercase()
4747
val required = schema.filter { it.role == roleStr && it.required }
48-
val missing = required.filter { entry ->
49-
val note = notesByKey[entry.key]
50-
note == null || note.body.isBlank()
51-
}
48+
val missing =
49+
required.filter { entry ->
50+
val note = notesByKey[entry.key]
51+
note == null || note.body.isBlank()
52+
}
5253

5354
return PhaseNoteContext(
5455
guidancePointer = missing.firstOrNull()?.guidance,

0 commit comments

Comments
 (0)