Skip to content

Filter symlinks by agent, expand instructions, generate opsx agent#4

Merged
ClankerGuru merged 6 commits into
mainfrom
fix/agent-aware-skill-generation
Apr 15, 2026
Merged

Filter symlinks by agent, expand instructions, generate opsx agent#4
ClankerGuru merged 6 commits into
mainfrom
fix/agent-aware-skill-generation

Conversation

@ClankerGuru
Copy link
Copy Markdown
Owner

@ClankerGuru ClankerGuru commented Apr 15, 2026

Summary

  • Symlink filtering: opsx-sync only generates symlinks for the configured defaultAgent, not all 4 agents
  • Instruction overhaul: Generated CLAUDE.md/AGENTS.md now include full onboarding — what opsx is, srcx context explanation, change lifecycle, strict MUST/MUST NOT rules
  • opsx agent: Selectable agent definition at .claude/agents/opsx.md and .github/agents/opsx.md with lifecycle knowledge

Test plan

  • ./gradlew build passes
  • opsx-sync outputs "symlinked to claude" (single agent)
  • .claude/agents/opsx.md generated with frontmatter and prompt
  • No .codex/ or .opencode/ dirs created

Change proposed via opsx

opsx-propose + opsx-apply with manual task refinement.

Summary by CodeRabbit

  • New Features

    • Configure a default agent (e.g., Claude or Copilot) to drive sync and generation.
    • Agent-specific instruction and agent-definition files are generated only for the selected agent; a universal AGENTS.md is always written.
    • Agent definitions vary by agent (e.g., Claude front-matter vs Copilot header).
  • Bug Fixes

    • Symlink creation and cleanup target only the active agent's directories and respect .gitignore patterns.
    • Agent process output capture/join behavior improved for more reliable logging.
  • Tests

    • Added/updated tests validating agent-specific generation and behaviors.

- SkillGenerator accepts defaultAgent, only generates symlinks for
  the configured agent instead of all 4
- Instruction files (CLAUDE.md/AGENTS.md) now include full onboarding:
  what opsx is, how srcx context works, change lifecycle, strict rules
- Generate selectable opsx agent at .claude/agents/opsx.md and
  .github/agents/opsx.md with lifecycle knowledge and rules
- SyncTask wires defaultAgent from extension
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@ClankerGuru has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 27 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 27 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41d49349-2c90-43da-81e5-f8b5dd426aec

📥 Commits

Reviewing files that changed from the base of the PR and between b90bc81 and a1e12a8.

📒 Files selected for processing (21)
  • README.md
  • 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/ApplyTask.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/OnboardTask.kt
  • src/main/kotlin/zone/clanker/opsx/task/ProposeTask.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/TaskExecutor.kt
  • src/test/kotlin/zone/clanker/opsx/model/AgentTest.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/workflow/AgentDispatcherTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/AtomicTaskLifecycleTest.kt
  • src/test/kotlin/zone/clanker/opsx/workflow/TaskExecutorTest.kt
📝 Walkthrough

Walkthrough

Propagates a defaultAgent from the Opsx extension into the opsx-sync task and SkillGenerator, changing sync, symlink, instruction-file, and agent-definition generation/cleanup to operate for the selected agent only, erroring on unknown agents.

Changes

Cohort / File(s) Summary
Plugin task registration
src/main/kotlin/zone/clanker/opsx/Opsx.kt
Sets the opsx-sync task's defaultAgent property from the plugin extension at registration.
Sync task
src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
Adds abstract val defaultAgent: Property<String>; run() reads defaultAgent, limits cleanup/symlink operations to the active agent's skill dirs, and errors on unknown agents.
Skill generation core
src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
Adds defaultAgent constructor param and activeTarget(); replaces AGENT_TARGETS with AGENT_CONFIG + AgentTarget; updates generatedDirs/instructionFiles to accept optional agent filter; generates symlinks, instruction files, and agent definitions only for the active agent; introduces generateAgentDefinitions() and buildAgentDefinition() with agent-specific content; updates "Rules" wording.
Agent dispatcher
src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt
Replaces direct process output redirection with a reader thread that logs lines to file and logger; adds a reader join timeout and forceful termination path; removes two informational log lines.
Tests — plugin & sync
src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
Test now sets task.defaultAgent = "claude" for the SyncTask run invocation.
Tests — skill generator
src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt, src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
Updated tests to pass defaultAgent to SkillGenerator; adjusted expectations so symlinks/instructions/definitions are created only for the active agent; added SkillGeneratorAgentTest to validate agent-specific files and error on unknown agents.

Sequence Diagram

sequenceDiagram
    participant Gradle as Gradle Task\n(opsx-sync)
    participant SyncTask as SyncTask
    participant SkillGen as SkillGenerator
    participant FS as FileSystem

    Gradle->>SyncTask: run() with defaultAgent
    SyncTask->>SkillGen: construct(defaultAgent)
    SyncTask->>SkillGen: request activeTarget()
    alt known agent
        SkillGen-->>SyncTask: AgentTarget(skillDir,instructionFile,agentDir)
        SyncTask->>SkillGen: generateSkillFiles()
        SkillGen->>FS: create symlinks in active skillDir only
        SyncTask->>SkillGen: generateInstructionFiles()
        SkillGen->>FS: write AGENTS.md and agent-specific file (if any)
        SyncTask->>SkillGen: generateAgentDefinitions()
        SkillGen->>FS: write opsx.md to active agentDir (if applicable)
        SyncTask->>FS: cleanup symlinks/.gitignore in active agent dirs only
    else unknown agent
        SkillGen-->>SyncTask: throw error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble through code, one agent to find,
Claude or Copilot, each neatly assigned,
Symlinks hop home and instructions align,
One default to guide every curious mind,
Hooray — tidy burrows for agents combined!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: filtering symlinks by agent, expanding instruction content, and generating opsx agent definitions.

✏️ 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 fix/agent-aware-skill-generation

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.

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.

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

99-109: Consider handling broken symlinks in isOpsxSymlink.

The runCatching block returns false on exception, which is reasonable. However, Files.readSymbolicLink doesn't resolve the target—it returns the raw path stored in the symlink. If the symlink was created with an absolute path, the comparison target.startsWith(sourceDir.toPath()) works correctly. But if it was created with a relative path, the comparison may fail unexpectedly.

The current implementation is likely fine since generateSkillFiles creates symlinks using absolute paths (source.toPath()), but this is worth noting if the behavior changes.

🤖 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/SyncTask.kt` around lines 99 - 109,
isOpsxSymlink currently reads the raw symlink target via Files.readSymbolicLink
and compares it directly to sourceDir, which fails for relative symlinks; update
isOpsxSymlink to resolve the read target against the symlink's parent (e.g.,
path.parent.resolve(target).normalize()) before comparing with
sourceDir.toPath(), and keep the existing runCatching/exception-to-false
behavior to treat broken/unresolvable symlinks as non-opsx symlinks; note
generateSkillFiles creates absolute links today but ensure the new resolution
covers relative links too.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt`:
- Around line 99-109: isOpsxSymlink currently reads the raw symlink target via
Files.readSymbolicLink and compares it directly to sourceDir, which fails for
relative symlinks; update isOpsxSymlink to resolve the read target against the
symlink's parent (e.g., path.parent.resolve(target).normalize()) before
comparing with sourceDir.toPath(), and keep the existing
runCatching/exception-to-false behavior to treat broken/unresolvable symlinks as
non-opsx symlinks; note generateSkillFiles creates absolute links today but
ensure the new resolution covers relative links too.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c01b9cb-30c0-40f0-8465-8677bc10902c

📥 Commits

Reviewing files that changed from the base of the PR and between 65c7b63 and 5af5c3f.

📒 Files selected for processing (6)
  • src/main/kotlin/zone/clanker/opsx/Opsx.kt
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/main/kotlin/zone/clanker/opsx/task/SyncTask.kt
  • src/test/kotlin/zone/clanker/opsx/OpsxPluginTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt

CLAUDE.md/AGENTS.md now explain what opsx/srcx/wrkx are, how to
read srcx context (hub classes, entry points, interfaces, anti-patterns,
cross-build), the change lifecycle with status explanations, and the
change directory structure.

Agent definition (.claude/agents/opsx.md) now includes "Before You Do
Anything" section, explains the three integrated systems, and has
strict MUST/MUST NOT rules.
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: 2

🤖 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/skill/SkillGenerator.kt`:
- Around line 138-148: Before writing the active agent files, remove or unmark
artifacts belonging to any inactive targets so old agent integrations are not
left behind: call activeTarget() to get the current target, iterate over the
list of all known targets (the same source used to choose activeTarget()), and
for each target != activeTarget() delete or clear their instructionFile and
agent file locations (the same paths constructed into agentFile and the
per-target agent/markdown locations you write with writeWithMarkers). Ensure you
remove empty parent directories after deletion and apply the same cleanup logic
to the other write block referenced around lines 151-156 so inactive
.md/.github/ or .agent files are removed or unmarked before writing the active
set.
- Around line 195-221: The generated help text in SkillGenerator (where the
agent prompt is assembled via appendLine calls) uses bare commands like
`opsx-status`, `opsx-propose`, `opsx-apply`, etc., which are incorrect; replace
those literal occurrences with the documented invocation form by reusing the
existing TASK_USAGE constant (or the same source used to build TASK_USAGE) when
assembling the lines (e.g., use TASK_USAGE + " status"/TASK_USAGE + " propose"
or format the command placeholders from TASK_USAGE) so the examples become
`/opsx-*` or `./gradlew -q opsx-*` consistently; update the additional commands
line likewise to reference TASK_USAGE rather than hardcoded names so the docs
never drift.
🪄 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: 913f0a56-cae1-4332-9571-96b41fed8c1d

📥 Commits

Reviewing files that changed from the base of the PR and between 5af5c3f and dd5ac01.

📒 Files selected for processing (3)
  • src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt
✅ Files skipped from review due to trivial changes (1)
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorAgentTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/kotlin/zone/clanker/opsx/skill/SkillGeneratorTest.kt

Comment thread src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt
Comment thread src/main/kotlin/zone/clanker/opsx/skill/SkillGenerator.kt Outdated
ProcessBuilder.redirectOutput(appendTo) caused waitFor() to hang
because the JVM held the file descriptor open after process exit.
Now output is read via a daemon thread that writes to both the log
file and Gradle logger, allowing waitFor() to return immediately
when the process exits.
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: 2

🧹 Nitpick comments (1)
src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt (1)

50-57: Avoid per-line appendText in the hot output loop.

Line 55 reopens/writes/closes the file for every line. Under verbose agent output this can become a bottleneck and delay stream consumption.

Refactor to keep a single writer open
-                val reader =
-                    Thread {
-                        process.inputStream.bufferedReader().useLines { lines ->
-                            lines.forEach { line ->
-                                logFile.appendText(line + "\n")
-                                logger.quiet(line)
-                            }
-                        }
-                    }
+                val reader =
+                    Thread {
+                        process.inputStream.bufferedReader().useLines { lines ->
+                            logFile.bufferedWriter().use { writer ->
+                                lines.forEach { line ->
+                                    writer.appendLine(line)
+                                    logger.quiet(line)
+                                }
+                            }
+                        }
+                    }
🤖 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/AgentDispatcher.kt` around lines
50 - 57, The hot loop in AgentDispatcher's reader thread reopens the file per
line via logFile.appendText; change the code in the Thread created around
process.inputStream.bufferedReader().useLines so it opens a single
BufferedWriter (or FileWriter with append=true) before iterating lines, write
each line to that writer and call writer.newLine()/flush periodically, and close
the writer after the loop; keep logger.quiet(line) per line but remove per-line
logFile.appendText calls to avoid repeated open/close overhead.
🤖 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/AgentDispatcher.kt`:
- Line 65: After the bounded join call reader.join(READER_JOIN_TIMEOUT_MS) add
an explicit liveness check: if reader.isAlive() then log a clear error/warn
(include context like "reader thread did not terminate after
READER_JOIN_TIMEOUT_MS"), call reader.interrupt() and attempt a short follow-up
join (e.g., reader.join(READER_JOIN_TIMEOUT_MS / 10)); if still alive, escalate
(log error and/or throw/track for shutdown). Apply the same pattern to the other
join at the same site (the writer thread join around line with writer.join and
its timeout) to avoid leaving dangling threads and truncated logs.
- Around line 63-67: The code currently returns Result(-1, logFile) for both
timeouts and exception fallbacks, conflating distinct failure modes; modify the
Result data class (or create a companion/overload) to include an explicit
failure reason (e.g., a FailureReason enum with values like TIMEOUT,
LAUNCH_ERROR, RUNTIME_ERROR, SUCCESS) and set that field instead of overloading
-1. Update the two return sites in AgentDispatcher's runCatching block (the
timeout branch that calls Result(-1, logFile) and the exception fallback) to
return Result(..., failureReason = FailureReason.TIMEOUT) and Result(...,
failureReason = FailureReason.LAUNCH_ERROR/ RUNTIME_ERROR as appropriate), and
then update callers such as TaskExecutor and VerifyTask to read the new
failureReason instead of interpreting -1.

---

Nitpick comments:
In `@src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt`:
- Around line 50-57: The hot loop in AgentDispatcher's reader thread reopens the
file per line via logFile.appendText; change the code in the Thread created
around process.inputStream.bufferedReader().useLines so it opens a single
BufferedWriter (or FileWriter with append=true) before iterating lines, write
each line to that writer and call writer.newLine()/flush periodically, and close
the writer after the loop; keep logger.quiet(line) per line but remove per-line
logFile.appendText calls to avoid repeated open/close overhead.
🪄 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: 349d1b8f-76e5-49b1-ae43-59898501b102

📥 Commits

Reviewing files that changed from the base of the PR and between dd5ac01 and b90bc81.

📒 Files selected for processing (1)
  • src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt

Comment thread src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt Outdated
Comment thread src/main/kotlin/zone/clanker/opsx/workflow/AgentDispatcher.kt Outdated
- Agent enum (CLAUDE, COPILOT, CODEX, OPENCODE) replaces string-based
  agent config throughout codebase
- SkillGenerator accepts additionalSourceDirs to merge skills from
  company/project dirs into ~/.clkx/skills/
- AgentDispatcher.dispatch takes Agent enum, not String
- TaskExecutor uses Agent enum
- All task callers updated to use Agent.fromId()
CleanTask now removes project-level symlinks for ALL agents (not just
home dirs), plus agent definition files. README updated with Agent.kt
in project structure listing.
- SkillGenerator cleans inactive agent instruction files and definitions
  before writing the active set (prevents stale artifacts when switching)
- AgentDispatcher.Result gains FailureReason enum (TIMEOUT vs DISPATCH_EXCEPTION)
- Reader thread gets explicit liveness check + interrupt after join timeout
- Agent prompt uses proper /command and ./gradlew forms, not bare names
@ClankerGuru ClankerGuru merged commit 30819a2 into main Apr 15, 2026
2 checks passed
@ClankerGuru ClankerGuru deleted the fix/agent-aware-skill-generation branch April 15, 2026 18:02
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