Skip to content

Commit b21c012

Browse files
jpicklykclaude
andcommitted
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>
1 parent 52c5b35 commit b21c012

File tree

110 files changed

+19549
-15783
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

+19549
-15783
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)