Skip to content

feat: migrate opsx from Gradle plugin to standalone CLI + TUI (v0.50.0)#9

Merged
ClankerGuru merged 1 commit into
mainfrom
stream-agent-output
Apr 28, 2026
Merged

feat: migrate opsx from Gradle plugin to standalone CLI + TUI (v0.50.0)#9
ClankerGuru merged 1 commit into
mainfrom
stream-agent-output

Conversation

@ClankerGuru
Copy link
Copy Markdown
Owner

@ClankerGuru ClankerGuru commented Apr 16, 2026

Summary

Complete rewrite of opsx from a Gradle settings plugin to a standalone Kotlin CLI application with a full-screen TUI.

  • cli/ — headless domain logic organized by feature (init, install, update, nuke, status, log, list)
  • tui/ — full-screen interactive TUI with Mordant widgets, gum-style confirm dialogs, resize support
  • app/ — thin entry point wiring CLI commands and TUI dashboard

What changed

  • Full-screen TUI with ASCII logo, gradient colors, SelectList menu, vim keys (j/k/h/l)
  • opsx init writes skills/agents directly to host directories (.claude/, .github/, .codex-plugin/, .opencode/)
  • opsx nuke selectively removes only opsx-owned files using the manifest, preserving user content
  • opsx install copies binary to ~/.opsx/bin with PATH wiring and zsh completions
  • opsx update checks GitHub releases with download, checksum verification, and extraction
  • All file I/O via kotlinx-io (Path + SystemFileSystem), java.io.File forbidden
  • Convention plugins: clkx-kotlin, clkx-cli, clkx-app, clkx-lib, clkx-detekt, clkx-ktlint, clkx-kover, clkx-konsist
  • 95%+ test coverage on both cli and tui modules, no exclusions
  • Skills embedded as JAR resources (74 skills, 7 agents)
  • Reusable ConfirmDialog component matching Charmbracelet gum confirm UX
  • AppShell shared layout (logo, content, status bar) across all screens
  • Legacy OPSX:AUTO markers cleaned up alongside new >>> opsx >>> markers

Release

Tag as v0.50.0 on merge. Version in gradle.properties is 0.1.0-SNAPSHOT for development; the release workflow uses the git tag.

Test plan

  • ./gradlew clean check --no-build-cache passes (compile, detekt, ktlint, tests, kover)
  • cli coverage 95%+, tui coverage 95%+
  • opsx init --host claude --host copilot --host codex --host opencode installs to correct directories
  • opsx nuke --keep-rc removes only opsx-owned files
  • opsx install copies to ~/.opsx/bin, wires PATH
  • opsx update checks GitHub releases
  • TUI launches full-screen, all menu items work, resize supported
  • Pre-push hook passes (26s full test suite)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Important

Review skipped

Too many files!

This PR contains 279 files, which is 129 over the limit of 150.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6907a3c2-7068-4ac5-8ece-d111c55e4ccd

📥 Commits

Reviewing files that changed from the base of the PR and between 68e4556 and b72c75b.

📒 Files selected for processing (279)
  • .github/copilot-instructions.md
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • app/build.gradle.kts
  • app/src/main/kotlin/zone/clanker/opsx/cli/Main.kt
  • build-logic/build.gradle.kts
  • build-logic/src/main/kotlin/clkx-app.gradle.kts
  • build-logic/src/main/kotlin/clkx-cli.gradle.kts
  • build-logic/src/main/kotlin/clkx-conventions.gradle.kts
  • build-logic/src/main/kotlin/clkx-cover.gradle.kts
  • build-logic/src/main/kotlin/clkx-detekt.gradle.kts
  • build-logic/src/main/kotlin/clkx-konsist.gradle.kts
  • build-logic/src/main/kotlin/clkx-kotlin.gradle.kts
  • build-logic/src/main/kotlin/clkx-kover.gradle.kts
  • build-logic/src/main/kotlin/clkx-lib.gradle.kts
  • build-logic/src/main/kotlin/clkx-module.gradle.kts
  • build-logic/src/main/kotlin/clkx-plugin.gradle.kts
  • build-logic/src/main/kotlin/clkx-publish.gradle.kts
  • build-logic/src/main/kotlin/clkx-testing.gradle.kts
  • build-logic/src/main/kotlin/clkx-toolchain.gradle.kts
  • build-logic/src/main/kotlin/zone/clanker/gradle/conventions/ClkxSettingsPlugin.kt
  • build-logic/src/main/kotlin/zone/clanker/gradle/conventions/ShellcheckTask.kt
  • build.gradle.kts
  • changes/fix-configuration-cache-compatibility/design.md
  • changes/fix-configuration-cache-compatibility/proposal.md
  • changes/fix-configuration-cache-compatibility/tasks.md
  • cli/build.gradle.kts
  • cli/detekt.yml
  • cli/gradle.properties
  • cli/src/main/kotlin/zone/clanker/opsx/cli/ExitCode.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/OpsxCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/completion/CompletionCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/InitCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/InitRunner.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/config/Manifest.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/config/MarkerBlock.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/config/OpsxConfig.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/config/ResourceTree.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/emitter/ClaudeEmitter.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/emitter/CodexEmitter.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/emitter/CopilotEmitter.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/emitter/HostEmitter.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/emitter/OpenCodeEmitter.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/host/Host.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/init/host/HostDetector.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/install/InstallCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/install/InstallRunner.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/list/ListCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/log/LogCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/nuke/NukeCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/nuke/NukeRunner.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/status/ActivityLog.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/status/ChangeEntry.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/status/ChangeScanner.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/status/StatusCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/status/Style.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/update/UpdateCommand.kt
  • cli/src/main/kotlin/zone/clanker/opsx/cli/update/UpdateRunner.kt
  • cli/src/main/resources/content/agents/architect.md
  • cli/src/main/resources/content/agents/devOps.md
  • cli/src/main/resources/content/agents/developer.md
  • cli/src/main/resources/content/agents/forge.md
  • cli/src/main/resources/content/agents/lead.md
  • cli/src/main/resources/content/agents/qa.md
  • cli/src/main/resources/content/agents/scout.md
  • cli/src/main/resources/content/bin/_opsx-host.sh
  • cli/src/main/resources/content/bin/_opsx-style.sh
  • cli/src/main/resources/content/bin/opsx-log
  • cli/src/main/resources/content/bin/opsx-render
  • cli/src/main/resources/content/bin/opsx-status
  • cli/src/main/resources/content/manifest.json
  • cli/src/main/resources/content/skills/_fragments/activity-setup.md
  • cli/src/main/resources/content/skills/ai-cli-subagents/SKILL.md
  • cli/src/main/resources/content/skills/charm-glow/SKILL.md
  • cli/src/main/resources/content/skills/charm-gum/SKILL.md
  • cli/src/main/resources/content/skills/charm-skate/SKILL.md
  • cli/src/main/resources/content/skills/charm-vhs/SKILL.md
  • cli/src/main/resources/content/skills/clikt/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-cancellation-exceptions/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-channels-select/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-context-dispatchers/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-flow/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-scopes/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-shared-state/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-stateflow-sharedflow/SKILL.md
  • cli/src/main/resources/content/skills/coroutines-suspend-functions/SKILL.md
  • cli/src/main/resources/content/skills/detekt/SKILL.md
  • cli/src/main/resources/content/skills/exposed/SKILL.md
  • cli/src/main/resources/content/skills/gh-cli/SKILL.md
  • cli/src/main/resources/content/skills/git-workflow/SKILL.md
  • cli/src/main/resources/content/skills/github-actions/SKILL.md
  • cli/src/main/resources/content/skills/go/SKILL.md
  • cli/src/main/resources/content/skills/gradle-build-conventions/SKILL.md
  • cli/src/main/resources/content/skills/gradle-composite-builds/SKILL.md
  • cli/src/main/resources/content/skills/gradle-custom-plugins/SKILL.md
  • cli/src/main/resources/content/skills/gradle-dependency-injection/SKILL.md
  • cli/src/main/resources/content/skills/gradle-init-scripts/SKILL.md
  • cli/src/main/resources/content/skills/gradle-plugins-basics/SKILL.md
  • cli/src/main/resources/content/skills/gradle-providers-properties/SKILL.md
  • cli/src/main/resources/content/skills/gradle-settings-plugin/SKILL.md
  • cli/src/main/resources/content/skills/gradle-tasks/SKILL.md
  • cli/src/main/resources/content/skills/gradle/SKILL.md
  • cli/src/main/resources/content/skills/konsist/SKILL.md
  • cli/src/main/resources/content/skills/kotest/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-conventions/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-dsl-builders/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-dsl/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-functional-first/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-lang/SKILL.md
  • cli/src/main/resources/content/skills/kotlin-multiplatform/SKILL.md
  • cli/src/main/resources/content/skills/kotlinx-coroutines/SKILL.md
  • cli/src/main/resources/content/skills/kotlinx-io/SKILL.md
  • cli/src/main/resources/content/skills/kotlinx-serialization/SKILL.md
  • cli/src/main/resources/content/skills/kover/SKILL.md
  • cli/src/main/resources/content/skills/ktlint/SKILL.md
  • cli/src/main/resources/content/skills/ktor-client/SKILL.md
  • cli/src/main/resources/content/skills/ktor-plugin/SKILL.md
  • cli/src/main/resources/content/skills/ktor-server/SKILL.md
  • cli/src/main/resources/content/skills/mordant-tui/SKILL.md
  • cli/src/main/resources/content/skills/mosaic/SKILL.md
  • cli/src/main/resources/content/skills/naming-conventions/SKILL.md
  • cli/src/main/resources/content/skills/no-python/SKILL.md
  • cli/src/main/resources/content/skills/no-temp-scripts/SKILL.md
  • cli/src/main/resources/content/skills/okio/SKILL.md
  • cli/src/main/resources/content/skills/opsx-apply/SKILL.md
  • cli/src/main/resources/content/skills/opsx-archive/SKILL.md
  • cli/src/main/resources/content/skills/opsx-bulk-archive/SKILL.md
  • cli/src/main/resources/content/skills/opsx-continue/SKILL.md
  • cli/src/main/resources/content/skills/opsx-explore/SKILL.md
  • cli/src/main/resources/content/skills/opsx-feedback/SKILL.md
  • cli/src/main/resources/content/skills/opsx-ff/SKILL.md
  • cli/src/main/resources/content/skills/opsx-onboard/SKILL.md
  • cli/src/main/resources/content/skills/opsx-propose/SKILL.md
  • cli/src/main/resources/content/skills/opsx-status/SKILL.md
  • cli/src/main/resources/content/skills/opsx-verify/SKILL.md
  • cli/src/main/resources/content/skills/package-structure/SKILL.md
  • cli/src/main/resources/content/skills/serialization-patterns/SKILL.md
  • cli/src/main/resources/content/skills/srcx-clean/SKILL.md
  • cli/src/main/resources/content/skills/srcx-context/SKILL.md
  • cli/src/main/resources/content/skills/testing-patterns/SKILL.md
  • cli/src/main/resources/content/skills/wrkx-checkout/SKILL.md
  • cli/src/main/resources/content/skills/wrkx-clone/SKILL.md
  • cli/src/main/resources/content/skills/wrkx-prune/SKILL.md
  • cli/src/main/resources/content/skills/wrkx-pull/SKILL.md
  • cli/src/main/resources/content/skills/wrkx-status/SKILL.md
  • cli/src/main/resources/content/skills/wrkx/SKILL.md
  • cli/src/test/kotlin/zone/clanker/opsx/cli/AllCommandsTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/ExitCodeTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/OpsxCommandTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/InitRunnerTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/config/ConfigLoaderTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/config/ManifestTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/config/MarkerBlockTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/config/ResourceTreeTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/emitter/ClaudeEmitterTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/emitter/CodexEmitterTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/emitter/CopilotEmitterTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/emitter/OpenCodeEmitterTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/host/HostDetectorTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/init/host/HostTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/install/InstallRunnerTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/log/LogCommandTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/nuke/NukeCommandTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/nuke/NukeRunnerGlobalTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/nuke/NukeRunnerHostTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/nuke/NukeRunnerTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/status/ActivityLogTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/status/ChangeScannerTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/status/StatusCommandTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/status/StyleTest.kt
  • cli/src/test/kotlin/zone/clanker/opsx/cli/update/UpdateRunnerTest.kt
  • config/detekt.yml
  • docs/agents.md
  • docs/tui-navigation.md
  • docs/tui-screens.md
  • gradle.properties
  • gradle/libs.versions.toml
  • install.sh
  • opsx/changes/atomic-task-execution/.opsx.yaml
  • opsx/changes/atomic-task-execution/design.md
  • opsx/changes/atomic-task-execution/proposal.md
  • opsx/changes/atomic-task-execution/tasks.md
  • run.sh
  • settings.gradle.kts
  • src/main/kotlin/zone/clanker/opsx/Opsx.kt
  • src/main/kotlin/zone/clanker/opsx/model/Agent.kt
  • src/main/kotlin/zone/clanker/opsx/model/Change.kt
  • src/main/kotlin/zone/clanker/opsx/model/ChangeConfig.kt
  • src/main/kotlin/zone/clanker/opsx/model/ChangeStatus.kt
  • src/main/kotlin/zone/clanker/opsx/model/OpsxConfig.kt
  • src/main/kotlin/zone/clanker/opsx/model/TaskDefinition.kt
  • src/main/kotlin/zone/clanker/opsx/model/TaskStatus.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/main/kotlin/zone/clanker/opsx/skill/TaskInfo.kt
  • src/main/kotlin/zone/clanker/opsx/task/ApplyTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ArchiveTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/BulkArchiveTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ContinueTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ExploreTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/FeedbackTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/FfTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ListTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/OnboardTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ProposeTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/StatusTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/VerifyTask.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/ChangeLogger.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/ChangeReader.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/ChangeWriter.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/PromptBuilder.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/TaskExecutor.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/TaskParser.kt
  • src/slopTest/kotlin/zone/clanker/opsx/ForbiddenPackageTest.kt
  • src/slopTest/kotlin/zone/clanker/opsx/ForbiddenPatternTest.kt
  • src/slopTest/kotlin/zone/clanker/opsx/NamingConventionTest.kt
  • src/slopTest/kotlin/zone/clanker/opsx/PackageBoundaryTest.kt
  • src/slopTest/kotlin/zone/clanker/opsx/TaskAnnotationTest.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/ChangeConfigTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/ChangeStatusTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/ChangeTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/TaskDefinitionTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/TaskStatusTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ApplyTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ArchiveTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/BulkArchiveTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/CleanTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ContinueTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ExploreTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/FeedbackTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/FfTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ListTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/OnboardTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/ProposeTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/StatusTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/SyncTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/VerifyTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AgentDispatcherTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AtomicTaskLifecycleTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/ChangeLoggerTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/ChangeReaderTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/ChangeWriterTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/PromptBuilderTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/TaskExecutorTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/TaskParserTest.kt
  • tui/build.gradle.kts
  • tui/src/main/kotlin/zone/clanker/opsx/tui/Dashboard.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/init/InitView.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/install/InstallView.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/nuke/NukeView.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/onboarding/OnboardingView.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/render/AppShell.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/render/ConfirmDialog.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/render/Styles.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/status/StatusView.kt
  • tui/src/main/kotlin/zone/clanker/opsx/tui/update/UpdateView.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/DashboardTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/init/InitViewTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/install/InstallViewTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/nuke/NukeViewTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/onboarding/OnboardingViewTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/render/AppShellTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/render/ConfirmDialogTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/render/KeysTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/render/StylesTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/status/StatusViewShowTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/status/StatusViewTest.kt
  • tui/src/test/kotlin/zone/clanker/opsx/tui/update/UpdateViewTest.kt

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Generate per-agent SKILL.md and three agent markdowns (opsx/srcx/wrkx) directly into each agent’s project directories, remove shared .clkx/.agents/skills symlink/copy flows, add AgentDetector for auto-detection, stream agent process output to the Gradle logger (remove per-agent log files), and update tests accordingly.

Changes

Cohort / File(s) Summary
Skill generation & distribution
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt, src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
Generate SKILL.md per agent under each agent’s skillsDir; include additional source dirs without copying to a shared location; remove shared .clkx/skills staging and symlink distribution.
Agent definition generation
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
Generate three per-agent markdown files (opsx.md, srcx.md, wrkx.md) directly into each agent’s agentDir; copy additional .md files into agents; delete stale per-agent files for inactive agents.
Agent dispatch / logging
src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt
Stop creating temp prompt/log files; pass prompt string to CLI; stream stdout/stderr to Gradle logger; remove logFile from Result; update buildCommand signature.
Agent detection & model enum
src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt, src/main/kotlin/zone/clanker/opsx/model/Agent.kt, src/main/kotlin/zone/clanker/opsx/Opsx.kt
Add AgentDetector.detect(...) (env + process-tree lookup) and use it as first fallback when resolving agent id; change Agent.CODEX.agentDir from .agents.codex/agents.
Cleanup & sync tasks
src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt, src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
Clean only project-level generated skill dirs and specific per-agent files (opsx.md,srcx.md,wrkx.md); remove legacy global .agents/skills and symlink removal flows; simplify sync to per-agent dirs and ignore writing shared .gitignore.
Tests — expectations, new suites & removals
src/test/kotlin/zone/clanker/opsx/... (many files: SkillGeneratorTest.kt, AgentDispatcherTest.kt, SkillDistributionMatrixTest.kt, AgentDetectorTest.kt, CleanTaskTest.kt, OpsxPlugin*, etc.)
Adapt tests to new per-agent file locations and AgentDispatcher.Result signature; add AgentDetectorTest and SkillDistributionMatrixTest; remove SyncTaskTest; update many assertions from symlink/shared-folder semantics to per-agent real files.
Minor updates
src/main/kotlin/zone/clanker/opsx/..., src/test/...
Small text/content changes in generated SKILL/command files (Execution section wording), adjust mocks and result constructions in tests, and minor path/test tweaks (remove user.home manipulation).

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,230,201,0.5)
    participant Gradle as Gradle Task
    participant Gen as SkillGenerator
    participant AgentStore as Agent Dir (project)
    participant AgentProc as Agent CLI Process
  end

  Gradle->>Gen: invoke generateSkillFiles(activeAgents, tasks, additionalDirs)
  Gen->>AgentStore: write `skillsDir/<skill>/SKILL.md` and `opsx.md/srcx.md/wrkx.md`
  Gradle->>AgentProc: buildCommand(agent, prompt, model) -> spawn CLI
  AgentProc-->>Gradle: stream stdout/stderr
  Gradle->>Gradle: logger.quiet(...) logs output (no per-agent log files)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and carved each agent a nook,
SKILLs tucked in folders—no shared-cave look.
Prompts fly as strings, logs stream straight to the log,
I polished old symlinks and swept out the fog.
A tidy warren of files — now back to my clover. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title describes a major migration (Gradle plugin to CLI+TUI) but the changeset is primarily about agent detection, directory structure fixes, and multi-agent definition generation—not a complete framework migration. Consider a more precise title like 'feat: add agent auto-detection and multi-agent definitions' or 'feat: migrate skill/agent generation to project-level directories' that reflects the actual scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stream-agent-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ClankerGuru ClankerGuru changed the title Stream agent output directly instead of temp files Agent detection, directory fixes, and multi-agent definitions Apr 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt (1)

69-80: Consider adding tests for detectFromProcessTree fallback.

The test verifies env detection succeeds but doesn't test the fallback path when env detection returns null and process tree detection is attempted. While process tree detection is harder to unit test (would require mocking ProcessHandle), consider adding at least a test that verifies detect() returns null when both detection methods fail.

Example additional test case
`when`("env detection fails and no process tree match") {
    val result = AgentDetector.detect(
        listOf(Agent.CLAUDE, Agent.COPILOT),
    ) { null }
    then("returns null") {
        // Process tree unlikely to match in test environment
        // This at least exercises the fallback path
        result shouldBe null
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt` around lines
69 - 80, Add a unit test that exercises the detect() fallback path by calling
AgentDetector.detect with the same candidate list (e.g., listOf(Agent.CLAUDE,
Agent.COPILOT)) but supplying an env resolver that returns null so env detection
fails; assert the result is null to verify that when detectFromProcessTree also
finds nothing detect() returns null (this will at least invoke the fallback
logic without needing to mock ProcessHandle).
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt (1)

236-244: Minor redundancy in file reference.

Line 237 writes to File(agentSourceDir, filename) and line 238 creates the same File object again as sourceFile. Consider reusing the file reference.

♻️ Suggested simplification
 GENERATED_AGENT_FILES.forEach { filename ->
-    File(agentSourceDir, filename).writeText(buildAgentFile(filename, agent))
     val sourceFile = File(agentSourceDir, filename)
+    sourceFile.writeText(buildAgentFile(filename, agent))
     if (agent.usesCopy) {
         sourceFile.copyTo(File(projectDir, filename), overwrite = true)
     } else {
         createSymlink(File(projectDir, filename), sourceFile)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt` around lines 236 -
244, The code creates the same File(agentSourceDir, filename) twice; instead
instantiate a single val sourceFile = File(agentSourceDir, filename) before
calling buildAgentFile/writeText and then reuse that sourceFile for the copyTo
or createSymlink branch (inside the loop over GENERATED_AGENT_FILES). Update the
loop that references GENERATED_AGENT_FILES, agentSourceDir, filename,
buildAgentFile, sourceFile, agent.usesCopy, projectDir, and createSymlink to
avoid recreating the File object.
src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt (2)

11-19: Consider extracting shared withHome helper to reduce duplication.

This withHome function is duplicated from SkillGeneratorAgentTest.kt. Consider extracting it to a shared test utility file to follow DRY principles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`
around lines 11 - 19, Extract the duplicated helper function withHome into a
shared test utility (e.g., TestUtils or TestHelpers) and replace the copies in
SkillDistributionMatrixTest and SkillGeneratorAgentTest with calls to that
shared helper; move the implementation (preserving behavior of capturing and
restoring System.getProperty("user.home")) into the new utility as a public
function and update both tests to import and use it to remove duplication.

21-26: deleteOnExit() may not clean up directories with contents.

The deleteOnExit() method only deletes empty directories. Since tests create files inside, these directories may persist after test runs. Consider using JUnit's @TempDir or Kotest's tempdir() extension for reliable cleanup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`
around lines 21 - 26, The tempDir function in SkillDistributionMatrixTest
(private fun tempDir) uses File.deleteOnExit(), which won't remove non-empty
dirs, causing test artifacts to persist; replace this helper with a proper
temporary-directory facility (e.g., JUnit's `@TempDir` field/parameter or Kotest's
tempdir() extension) and update tests to use the provided Path/File from
`@TempDir` or tempdir(), removing the custom tempDir function and any manual
deleteOnExit() calls so the test framework handles reliable cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt`:
- Around line 28-38: The process basename extraction in detectFromProcessTree
yields names like "claude.exe" on Windows so it never matches configured
cliCommand values; change the extraction at the `name` assignment in
detectFromProcessTree (inside AgentDetector) to strip a trailing ".exe" (e.g.,
apply removeSuffix(".exe") to the basename) before looking it up in `byCommand`
so "claude.exe" will match "claude".

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt`:
- Around line 138-140: Update the COPILOT test assertion so it checks the actual
file path instead of only the directory: replace the weak check in the test
block labeled then("writes source of truth to ~/.clkx/agents/opsx.md") that uses
File(tempDir, ".clkx/agents").exists() with an assertion that File(tempDir,
".clkx/agents/copilot/opsx.md").exists() shouldBe true so the test verifies the
specific file was created.
- Around line 182-184: The test currently asserts existence of
".clkx/agents/opsx.md" but CODEX uses a per-agent subdirectory; update the
assertion in SkillGeneratorAgentTest (the "then(\"writes source of truth to
~/.clkx/agents/opsx.md\")" block) to check for ".clkx/agents/codex/opsx.md"
instead (i.e. assert File(tempDir, ".clkx/agents/codex/opsx.md").exists()
shouldBe true) so the test matches the new per-agent layout.
- Around line 220-222: The test currently only asserts that the ".clkx/agents"
directory exists; update the assertion in the then block (test named "writes
source of truth to ~/.clkx/agents/opsx.md") to specifically verify the OPENCODE
output file exists by checking ".clkx/agents/opencode/opsx.md" (e.g. replace the
File(tempDir, ".clkx/agents").exists() assertion with a check that File(tempDir,
".clkx/agents/opencode/opsx.md").exists() is true).
- Around line 96-98: The test's assertion in SkillGeneratorAgentTest (the then
block "writes source of truth to ~/.clkx/agents/opsx.md") currently only checks
the directory exists; change it to assert the concrete file is created (e.g.,
assert File(tempDir, ".clkx/agents/claude/opsx.md").exists() or derive the agent
subfolder/name used by the test) so the test verifies the actual opsx.md file is
written; update the assertion in the then block accordingly to check the
specific file path instead of just the directory.

---

Nitpick comments:
In `@src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt`:
- Around line 236-244: The code creates the same File(agentSourceDir, filename)
twice; instead instantiate a single val sourceFile = File(agentSourceDir,
filename) before calling buildAgentFile/writeText and then reuse that sourceFile
for the copyTo or createSymlink branch (inside the loop over
GENERATED_AGENT_FILES). Update the loop that references GENERATED_AGENT_FILES,
agentSourceDir, filename, buildAgentFile, sourceFile, agent.usesCopy,
projectDir, and createSymlink to avoid recreating the File object.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`:
- Around line 11-19: Extract the duplicated helper function withHome into a
shared test utility (e.g., TestUtils or TestHelpers) and replace the copies in
SkillDistributionMatrixTest and SkillGeneratorAgentTest with calls to that
shared helper; move the implementation (preserving behavior of capturing and
restoring System.getProperty("user.home")) into the new utility as a public
function and update both tests to import and use it to remove duplication.
- Around line 21-26: The tempDir function in SkillDistributionMatrixTest
(private fun tempDir) uses File.deleteOnExit(), which won't remove non-empty
dirs, causing test artifacts to persist; replace this helper with a proper
temporary-directory facility (e.g., JUnit's `@TempDir` field/parameter or Kotest's
tempdir() extension) and update tests to use the provided Path/File from
`@TempDir` or tempdir(), removing the custom tempDir function and any manual
deleteOnExit() calls so the test framework handles reliable cleanup.

In `@src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt`:
- Around line 69-80: Add a unit test that exercises the detect() fallback path
by calling AgentDetector.detect with the same candidate list (e.g.,
listOf(Agent.CLAUDE, Agent.COPILOT)) but supplying an env resolver that returns
null so env detection fails; assert the result is null to verify that when
detectFromProcessTree also finds nothing detect() returns null (this will at
least invoke the fallback logic without needing to mock ProcessHandle).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18a42ff3-9838-4d64-a189-ed851b1c0c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 71344f1 and a3a67e1.

📒 Files selected for processing (15)
  • src/main/kotlin/zone/clanker/opsx/Opsx.kt
  • src/main/kotlin/zone/clanker/opsx/model/Agent.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/CleanTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AgentDispatcherTest.kt
✅ Files skipped from review due to trivial changes (2)
  • src/main/kotlin/zone/clanker/opsx/model/Agent.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/kotlin/zone/clanker/opsx/workflow/AgentDispatcherTest.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt

Comment on lines +28 to +38
internal fun detectFromProcessTree(configured: List<Agent>): Agent? {
val byCommand = configured.associateBy { it.cliCommand }
var handle: ProcessHandle? = ProcessHandle.current().parent().orElse(null)
while (handle != null) {
val cmd = handle.info().command().orElse(null) ?: break
val name = cmd.substringAfterLast("/").substringAfterLast("\\")
byCommand[name]?.let { return it }
handle = handle.parent().orElse(null)
}
return null
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Agent.cliCommand values don't include extensions
ast-grep --pattern 'enum class Agent($$$) {
  $$$
}'

Repository: ClankerGuru/opsx

Length of output: 42


🏁 Script executed:

fd -t f "Agent.kt" -o -name "*Agent*"

Repository: ClankerGuru/opsx

Length of output: 290


🏁 Script executed:

rg "enum class Agent|data class Agent|class Agent" -A 5

Repository: ClankerGuru/opsx

Length of output: 2867


🏁 Script executed:

cat src/main/kotlin/zone/clanker/opsx/model/Agent.kt

Repository: ClankerGuru/opsx

Length of output: 1441


Strip .exe extension from process command name for Windows compatibility.

On Windows, ProcessHandle.info().command() returns paths like C:\...\claude.exe. The basename extraction yields claude.exe, which won't match cliCommand values ("claude", "copilot", etc.). Add .removeSuffix(".exe") to the basename extraction:

Proposed fix
-            val name = cmd.substringAfterLast("/").substringAfterLast("\\")
+            val name = cmd.substringAfterLast("/").substringAfterLast("\\")
+                .removeSuffix(".exe")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal fun detectFromProcessTree(configured: List<Agent>): Agent? {
val byCommand = configured.associateBy { it.cliCommand }
var handle: ProcessHandle? = ProcessHandle.current().parent().orElse(null)
while (handle != null) {
val cmd = handle.info().command().orElse(null) ?: break
val name = cmd.substringAfterLast("/").substringAfterLast("\\")
byCommand[name]?.let { return it }
handle = handle.parent().orElse(null)
}
return null
}
internal fun detectFromProcessTree(configured: List<Agent>): Agent? {
val byCommand = configured.associateBy { it.cliCommand }
var handle: ProcessHandle? = ProcessHandle.current().parent().orElse(null)
while (handle != null) {
val cmd = handle.info().command().orElse(null) ?: break
val name = cmd.substringAfterLast("/").substringAfterLast("\\")
.removeSuffix(".exe")
byCommand[name]?.let { return it }
handle = handle.parent().orElse(null)
}
return null
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt` around lines 28
- 38, The process basename extraction in detectFromProcessTree yields names like
"claude.exe" on Windows so it never matches configured cliCommand values; change
the extraction at the `name` assignment in detectFromProcessTree (inside
AgentDetector) to strip a trailing ".exe" (e.g., apply removeSuffix(".exe") to
the basename) before looking it up in `byCommand` so "claude.exe" will match
"claude".

Comment thread src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt Outdated
Comment thread src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt Outdated
Comment thread src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt Outdated
Comment thread src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt (1)

149-151: Avoid duplicated generated-agent filename lists across modules.

AGENT_FILES here duplicates the generated file set already defined in src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt (see generateAgentDefinitions usage). Please centralize this list to a shared constant to prevent drift.

♻️ Suggested direction
-    companion object {
-        private val AGENT_FILES = listOf("opsx.md", "srcx.md", "wrkx.md")
-    }
+    companion object {
+        // Reuse a shared constant (e.g., from SkillGenerator or a common constants object)
+        private val AGENT_FILES = SkillGenerator.GENERATED_AGENT_FILES
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt` around lines 149 - 151,
The companion object constant AGENT_FILES in CleanTask duplicates the
generated-agent filename list defined in SkillGenerator (used by
generateAgentDefinitions); remove the duplicate here and reference a single
shared constant instead (e.g., add or export a public constant in SkillGenerator
or a new shared object like GeneratedAgentFiles) then update CleanTask to use
that shared constant (replace AGENT_FILES with the shared symbol) and adjust
imports; ensure only one authoritative list exists and delete the old
AGENT_FILES declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`:
- Around line 199-200: The test in SkillDistributionMatrixTest (the then block
asserting File(dir, ".clkx").exists() shouldBe false) only checks a
project-local path and doesn't ensure the real $HOME/.clkx wasn't written;
update the test to set a temporary isolated home by overriding the JVM system
property "user.home" (or create a separate test that inspects
Paths.get(System.getProperty("user.home"), ".clkx")) before running the code
under test, run the generation, and then assert the temp-home-derived path does
not exist (and restore the original "user.home" afterwards) so the actual
home-derived path is verified instead of just the project-local dir.
- Around line 62-66: The test in SkillDistributionMatrixTest claims to ensure
"skills are real files (not symlinks)" but only uses file.isFile; update the
assertion to explicitly check the path is not a symbolic link by converting the
File to a Path and asserting Files.isSymbolicLink(file.toPath()) is false for
each constructed File (use the same file variable built from agent.skillsDir and
task.name/SKILL.md); keep the existing isFile assertion and add this negative
symlink assertion so the test fails if a symlink was used.

In `@src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt`:
- Around line 76-88: Add a new test in AgentDetectorTest that covers the
fallback path by ensuring detectFromEnv returns null and the process-tree path
is exercised: call AgentDetector.detect(...) with an env-mapping lambda that
returns null for the relevant keys and arrange for detectFromProcessTree
(indirectly exercised by AgentDetector.detect) to identify a specific agent
(e.g., Agent.COPILOT); then assert the returned value equals that agent.
Reference the top-level function AgentDetector.detect and its helper behaviors
detectFromEnv / detectFromProcessTree and use concrete Agent enum values
(Agent.CLAUDE, Agent.COPILOT) so the test verifies the fallback branch.

---

Nitpick comments:
In `@src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt`:
- Around line 149-151: The companion object constant AGENT_FILES in CleanTask
duplicates the generated-agent filename list defined in SkillGenerator (used by
generateAgentDefinitions); remove the duplicate here and reference a single
shared constant instead (e.g., add or export a public constant in SkillGenerator
or a new shared object like GeneratedAgentFiles) then update CleanTask to use
that shared constant (replace AGENT_FILES with the shared symbol) and adjust
imports; ensure only one authoritative list exists and delete the old
AGENT_FILES declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7f15d2f-d9f1-4350-870a-01f3ca72ea2f

📥 Commits

Reviewing files that changed from the base of the PR and between a3a67e1 and 68e4556.

📒 Files selected for processing (13)
  • src/main/kotlin/zone/clanker/opsx/Opsx.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/main/kotlin/zone/clanker/opsx/task/CleanTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/CleanTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/task/SyncTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt
💤 Files with no reviewable changes (1)
  • src/test/kotlin/zone/clanker/opsx/task/SyncTaskTest.kt
✅ Files skipped from review due to trivial changes (1)
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/kotlin/zone/clanker/opsx/Opsx.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginSyncTest.kt
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDetector.kt
  • src/test/kotlin/zone/clanker/opsx/task/CleanTaskTest.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt

Comment on lines +62 to +66
then("active ${agent.id}: skills are real files (not symlinks)") {
sampleTasks.forEach { task ->
val file = File(dir, "${agent.skillsDir}/${task.name}/SKILL.md")
file.isFile shouldBe true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt | sed -n '50,75p'

Repository: ClankerGuru/opsx

Length of output: 1381


🏁 Script executed:

cat -n src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt | sed -n '195,210p'

Repository: ClankerGuru/opsx

Length of output: 451


🏁 Script executed:

head -30 src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt

Repository: ClankerGuru/opsx

Length of output: 1017


🌐 Web query:

java File.isFile() symlinks behavior follows

💡 Result:

The java.io.File.isFile method follows symbolic links (symlinks). It transparently redirects to the target of the symlink and tests whether the target is a normal (regular) file. Key behaviors: - If the symlink points to a regular file, isFile returns true. - If the symlink points to a directory, isFile returns false (use isDirectory instead, which also follows symlinks). - If the symlink is broken (target does not exist), isFile returns false (and exists also returns false). - Symlinks themselves are not considered "normal files"; the method checks the target's type after dereferencing. This is per the official Java documentation: "Unless otherwise specified, symbolic links are transparent to applications and operations on files that are symbolic links are automatically redirected to the target of the link." File.isFile is such an operation that accesses the filesystem. For symlink-aware checks without following links, use java.nio.file.Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS), which returns false for symlinks regardless of target.

Citations:


🌐 Web query:

java Files.isSymbolicLink() how to check if path is symlink

💡 Result:

To check if a path is a symbolic link in Java, use the static method java.nio.file.Files.isSymbolicLink(Path path). This method returns true if the file is a symbolic link, false if the file does not exist, is not a symbolic link, or it cannot be determined. Example usage: import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; Path path = Paths.get("/path/to/file"); boolean isSymlink = Files.isSymbolicLink(path); System.out.println("Is symbolic link: " + isSymlink); This method is part of the NIO.2 API (Java 7+) and does not follow the link—it checks the path itself. It may throw SecurityException if access is denied, but typically no IOException for the check itself. Note: BasicFileAttributes.isSymbolicLink (from Files.readAttributes) may differ if links are followed by default; use LinkOption.NOFOLLOW_LINKS to match Files.isSymbolicLink behavior. To read the symlink target, use Files.readSymbolicLink(path).

Citations:


🌐 Web query:

java Files.isRegularFile LinkOption.NOFOLLOW_LINKS vs isFile isSymbolicLink

💡 Result:

In Java NIO.2 (java.nio.file.Files), there is no Files.isFile method. The query likely refers to java.io.File.isFile for comparison. Files.isRegularFile(Path path, LinkOption... options): Tests if the path is a regular file (not directory, symlink, or special file like device/socket). By default (no options), follows symlinks: returns true if symlink target is regular file. With LinkOption.NOFOLLOW_LINKS, does not follow symlinks: returns true only if path itself is regular file (symlink returns false). Files.isSymbolicLink(Path path): Tests if path is a symbolic link. Does not follow symlinks (always examines the path itself). No LinkOption parameter. java.io.File.isFile: Tests if path denotes a normal file (not directory). Follows symlinks transparently (returns true if target is normal file). Less precise than isRegularFile (may return true for some special files). Key differences with NOFOLLOW_LINKS: - isRegularFile without option: follows links, checks target type. - isRegularFile(NOFOLLOW_LINKS): checks path itself, ignores target. - isSymbolicLink: always checks path itself for being a symlink. - isFile (java.io.File): always follows links. To check if path is regular file not symlink: !Files.isSymbolicLink(path) && Files.isRegularFile(path, NOFOLLOW_LINKS). Or Files.isRegularFile(path, NOFOLLOW_LINKS) alone (since symlinks aren't regular files).

Citations:


🏁 Script executed:

cat -n src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt | sed -n '25,50p'

Repository: ClankerGuru/opsx

Length of output: 1248


🏁 Script executed:

grep -n "val dir" src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt | head -5

Repository: ClankerGuru/opsx

Length of output: 113


🏁 Script executed:

cat -n src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt | wc -l

Repository: ClankerGuru/opsx

Length of output: 63


File.isFile() does not verify files are not symlinks; add explicit check.

The test name at line 62 claims to verify "skills are real files (not symlinks)", but line 65 only checks file.isFile shouldBe true. This passes even if the file is a symlink pointing to a regular file. To actually prevent symlink-based distribution, explicitly assert that the path is not a symbolic link.

Suggested fix
 import io.kotest.matchers.string.shouldContain
 import io.kotest.matchers.string.shouldNotContain
 import zone.clanker.opsx.model.Agent
 import java.io.File
+import java.nio.file.Files
 
                     then("active ${agent.id}: skills are real files (not symlinks)") {
                         sampleTasks.forEach { task ->
                             val file = File(dir, "${agent.skillsDir}/${task.name}/SKILL.md")
                             file.isFile shouldBe true
+                            Files.isSymbolicLink(file.toPath()) shouldBe false
                         }
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
then("active ${agent.id}: skills are real files (not symlinks)") {
sampleTasks.forEach { task ->
val file = File(dir, "${agent.skillsDir}/${task.name}/SKILL.md")
file.isFile shouldBe true
}
then("active ${agent.id}: skills are real files (not symlinks)") {
sampleTasks.forEach { task ->
val file = File(dir, "${agent.skillsDir}/${task.name}/SKILL.md")
file.isFile shouldBe true
Files.isSymbolicLink(file.toPath()) shouldBe false
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`
around lines 62 - 66, The test in SkillDistributionMatrixTest claims to ensure
"skills are real files (not symlinks)" but only uses file.isFile; update the
assertion to explicitly check the path is not a symbolic link by converting the
File to a Path and asserting Files.isSymbolicLink(file.toPath()) is false for
each constructed File (use the same file variable built from agent.skillsDir and
task.name/SKILL.md); keep the existing isFile assertion and add this negative
symlink assertion so the test fails if a symlink was used.

Comment on lines +199 to +200
then("no ~/.clkx/ directory created") {
File(dir, ".clkx").exists() shouldBe false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This does not verify that $HOME/.clkx stays untouched.

File(dir, ".clkx") only checks for a project-local legacy directory. If generation regressed and wrote to the real home directory again, this assertion would still pass. Point user.home at an isolated temp home for this case, or add a dedicated test around the actual home-derived path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/skill/SkillDistributionMatrixTest.kt`
around lines 199 - 200, The test in SkillDistributionMatrixTest (the then block
asserting File(dir, ".clkx").exists() shouldBe false) only checks a
project-local path and doesn't ensure the real $HOME/.clkx wasn't written;
update the test to set a temporary isolated home by overriding the JVM system
property "user.home" (or create a separate test that inspects
Paths.get(System.getProperty("user.home"), ".clkx")) before running the code
under test, run the generation, and then assert the temp-home-derived path does
not exist (and restore the original "user.home" afterwards) so the actual
home-derived path is verified instead of just the project-local dir.

Comment on lines +76 to +88
given("detect") {

`when`("env detection succeeds") {
val env = mapOf("CLAUDE_CODE_ENTRY_POINT" to "cli")
val result =
AgentDetector.detect(
listOf(Agent.CLAUDE, Agent.COPILOT),
) { env[it] }
then("returns env result without checking process tree") {
result shouldBe Agent.CLAUDE
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Process-tree fallback is still untested.

This only proves the env-hit branch of detect(). If detectFromEnv(...) returns null, the new detectFromProcessTree(...) path never runs anywhere in this suite, so the core fallback behavior can regress unnoticed. Please add an env-miss case that exercises the process-tree branch as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/zone/clanker/opsx/workflow/AgentDetectorTest.kt` around lines
76 - 88, Add a new test in AgentDetectorTest that covers the fallback path by
ensuring detectFromEnv returns null and the process-tree path is exercised: call
AgentDetector.detect(...) with an env-mapping lambda that returns null for the
relevant keys and arrange for detectFromProcessTree (indirectly exercised by
AgentDetector.detect) to identify a specific agent (e.g., Agent.COPILOT); then
assert the returned value equals that agent. Reference the top-level function
AgentDetector.detect and its helper behaviors detectFromEnv /
detectFromProcessTree and use concrete Agent enum values (Agent.CLAUDE,
Agent.COPILOT) so the test verifies the fallback branch.

@ClankerGuru ClankerGuru force-pushed the stream-agent-output branch 2 times, most recently from f333f88 to b4ce0ee Compare April 28, 2026 21:09
@ClankerGuru
Copy link
Copy Markdown
Owner Author

@coderabbitai review

         \\
          \\
           \\
            ╱|、
          (˚ˎ 。7
           |、˜〵
           じしˍ,)ノ

    Ready for review! 🥕
    
    This is a complete rewrite — Gradle plugin → standalone CLI + TUI.
    One squashed commit, 95%+ test coverage, all quality gates green.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

@ClankerGuru Sure thing! Triggering a full review of PR #9 now. 🐰🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ClankerGuru ClankerGuru force-pushed the stream-agent-output branch from b4ce0ee to 5c8845e Compare April 28, 2026 21:27
@ClankerGuru ClankerGuru changed the title Agent detection, directory fixes, and multi-agent definitions feat: migrate opsx from Gradle plugin to standalone CLI + TUI (v0.50.0) Apr 28, 2026
Replaces the opsx Gradle plugin with a standalone Kotlin CLI application
distributed as a shadow JAR. The new architecture splits into three modules:

- cli/ — headless domain logic organized by feature (init, install, update, nuke, status, log, list)
- tui/ — full-screen interactive TUI with Mordant widgets, gum-style confirm dialogs, resize support
- app/ — thin entry point wiring CLI commands and TUI dashboard

Key changes:
- Full-screen TUI with ASCII logo, gradient colors, SelectList menu, vim keys (j/k/h/l)
- Init writes skills/agents directly to host directories (.claude/, .github/, .codex-plugin/, .opencode/)
- Nuke selectively removes only opsx-owned files using manifest, preserving user content
- Install copies binary to ~/.opsx/bin with PATH wiring and zsh completions
- Update checks GitHub releases with download, checksum verification, and extraction
- All file I/O via kotlinx-io (Path + SystemFileSystem), java.io.File forbidden
- Convention plugins: clkx-kotlin, clkx-cli, clkx-app, clkx-lib, clkx-detekt, clkx-ktlint, clkx-kover, clkx-konsist
- 95%+ test coverage on both cli and tui modules
- Skills embedded as JAR resources including mordant-tui, kotlinx-io, okio references
- Reusable ConfirmDialog component matching Charmbracelet gum confirm UX
- AppShell shared layout (logo, content, status bar) across all screens
@ClankerGuru ClankerGuru force-pushed the stream-agent-output branch from 5c8845e to b72c75b Compare April 28, 2026 21:35
@ClankerGuru ClankerGuru merged commit 3cb5011 into main Apr 28, 2026
2 checks passed
@ClankerGuru ClankerGuru deleted the stream-agent-output branch April 28, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant