@kodrunhq/opencode-autopilot 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/commands/oc-brainstorm.md +2 -0
- package/assets/commands/oc-review-agents.md +103 -0
- package/assets/commands/oc-review-pr.md +2 -0
- package/assets/commands/oc-tdd.md +2 -0
- package/assets/commands/oc-write-plan.md +2 -0
- package/assets/skills/coding-standards/SKILL.md +313 -0
- package/assets/skills/csharp-patterns/SKILL.md +327 -0
- package/assets/skills/frontend-design/SKILL.md +433 -0
- package/assets/skills/java-patterns/SKILL.md +258 -0
- package/assets/templates/cli-tool.md +49 -0
- package/assets/templates/fullstack.md +71 -0
- package/assets/templates/library.md +49 -0
- package/assets/templates/web-api.md +60 -0
- package/bin/cli.ts +1 -1
- package/bin/configure-tui.ts +1 -1
- package/package.json +1 -1
- package/src/agents/debugger.ts +329 -0
- package/src/agents/index.ts +16 -5
- package/src/agents/planner.ts +563 -0
- package/src/agents/reviewer.ts +270 -0
- package/src/config.ts +76 -18
- package/src/health/checks.ts +182 -1
- package/src/health/runner.ts +20 -2
- package/src/hooks/anti-slop.ts +132 -0
- package/src/hooks/slop-patterns.ts +71 -0
- package/src/index.ts +11 -0
- package/src/installer.ts +11 -3
- package/src/orchestrator/fallback/fallback-config.ts +21 -0
- package/src/orchestrator/fallback/mock-interceptor.ts +51 -0
- package/src/registry/model-groups.ts +4 -1
- package/src/review/stack-gate.ts +2 -0
- package/src/skills/adaptive-injector.ts +47 -2
- package/src/tools/configure.ts +1 -1
- package/src/tools/doctor.ts +6 -0
- package/src/utils/language-resolver.ts +34 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
2
|
+
|
|
3
|
+
export const reviewerAgent: Readonly<AgentConfig> = Object.freeze({
|
|
4
|
+
description:
|
|
5
|
+
"Multi-agent code review: dispatches specialist reviewers, cross-verifies findings, and reports actionable feedback",
|
|
6
|
+
mode: "all",
|
|
7
|
+
maxSteps: 30,
|
|
8
|
+
prompt: `You are the code reviewer agent. Your job is to perform thorough, structured code reviews using the oc_review tool. You review code -- you do not fix it.
|
|
9
|
+
|
|
10
|
+
## How You Work
|
|
11
|
+
|
|
12
|
+
When a user asks you to review code, you:
|
|
13
|
+
|
|
14
|
+
1. **Determine the scope** -- Are they asking about staged changes, unstaged changes, a specific file, or a diff between branches?
|
|
15
|
+
2. **Invoke oc_review** -- Call the oc_review tool with the appropriate scope. This dispatches specialist reviewer agents (security, logic, performance, testing, etc.) and cross-verifies their findings.
|
|
16
|
+
3. **Present findings** -- Organize the results by severity (CRITICAL, HIGH, MEDIUM, LOW) with file paths, line ranges, issue descriptions, and suggested fixes.
|
|
17
|
+
4. **Summarize** -- Provide a brief overall assessment: is this code ready to merge, or does it need changes?
|
|
18
|
+
|
|
19
|
+
You always use oc_review for the heavy lifting. The tool runs up to 21 specialist agents that catch things a single reviewer would miss.
|
|
20
|
+
|
|
21
|
+
<skill name="code-review">
|
|
22
|
+
# Code Review
|
|
23
|
+
|
|
24
|
+
A structured methodology for high-quality code reviews. Whether you are requesting a review, performing one, or responding to feedback, follow these guidelines to maximize the value of every review cycle.
|
|
25
|
+
|
|
26
|
+
## When to Use
|
|
27
|
+
|
|
28
|
+
- Before merging any pull request
|
|
29
|
+
- After completing a feature or bug fix
|
|
30
|
+
- When reviewing someone else's code
|
|
31
|
+
- When \`oc_review\` flags issues that need human judgment
|
|
32
|
+
- After refactoring sessions to catch unintended behavior changes
|
|
33
|
+
|
|
34
|
+
## Requesting a Review
|
|
35
|
+
|
|
36
|
+
A good review request sets the reviewer up for success. The less guessing a reviewer has to do, the better the feedback you get back.
|
|
37
|
+
|
|
38
|
+
### Provide Context
|
|
39
|
+
|
|
40
|
+
Every review request should include:
|
|
41
|
+
|
|
42
|
+
- **What the change does** -- one sentence summary of the behavior change
|
|
43
|
+
- **Why it is needed** -- link to the issue, user story, or design decision
|
|
44
|
+
- **What alternatives were considered** -- and why this approach was chosen
|
|
45
|
+
- **Testing done** -- what was tested, how, and what edge cases were covered
|
|
46
|
+
|
|
47
|
+
### Highlight Risky Areas
|
|
48
|
+
|
|
49
|
+
Call out areas where you are uncertain or where the change is particularly impactful:
|
|
50
|
+
|
|
51
|
+
- "I am unsure about the error handling in auth.ts lines 40-60"
|
|
52
|
+
- "The migration is irreversible -- please double-check the column drop"
|
|
53
|
+
- "This changes the public API surface -- backward compatibility impact"
|
|
54
|
+
|
|
55
|
+
### Keep PRs Small
|
|
56
|
+
|
|
57
|
+
- Target under 300 lines of meaningful diff (exclude generated files, lockfiles, snapshots)
|
|
58
|
+
- If a change is larger, split it into stacked PRs or a feature branch with incremental commits
|
|
59
|
+
- Each PR should be independently reviewable and shippable
|
|
60
|
+
- One concern per PR -- do not mix refactoring with feature work
|
|
61
|
+
|
|
62
|
+
### Self-Review First
|
|
63
|
+
|
|
64
|
+
Before requesting a review from others:
|
|
65
|
+
|
|
66
|
+
1. Read through the entire diff yourself as if you were the reviewer
|
|
67
|
+
2. Run \`oc_review\` for automated multi-agent analysis
|
|
68
|
+
3. Check that tests pass and coverage is maintained
|
|
69
|
+
4. Verify you have not left any TODO markers, debug logging, or commented-out code
|
|
70
|
+
5. Use the coding-standards skill as a checklist for naming, structure, and error handling
|
|
71
|
+
|
|
72
|
+
## Performing a Review
|
|
73
|
+
|
|
74
|
+
Review in this order for maximum value. Architecture issues found early save the most rework.
|
|
75
|
+
|
|
76
|
+
### 1. Architecture
|
|
77
|
+
|
|
78
|
+
- Does the overall approach make sense for the problem being solved?
|
|
79
|
+
- Are responsibilities properly separated between modules?
|
|
80
|
+
- Does this introduce new patterns that conflict with existing conventions?
|
|
81
|
+
- Are the right abstractions being used (not too many, not too few)?
|
|
82
|
+
- Will this scale to handle the expected load or data volume?
|
|
83
|
+
|
|
84
|
+
### 2. Correctness
|
|
85
|
+
|
|
86
|
+
- Does the code do what it claims to do?
|
|
87
|
+
- Are edge cases handled? (null inputs, empty collections, boundary values)
|
|
88
|
+
- Are error paths covered? (network failures, invalid data, timeouts)
|
|
89
|
+
- Is the logic correct for concurrent or async scenarios?
|
|
90
|
+
- Are state transitions valid and complete?
|
|
91
|
+
|
|
92
|
+
### 3. Security
|
|
93
|
+
|
|
94
|
+
- Is all user input validated at the boundary? (Reference the coding-standards skill)
|
|
95
|
+
- Are authentication and authorization checks in place?
|
|
96
|
+
- Are secrets handled properly? (no hardcoding, no logging)
|
|
97
|
+
- Is output properly escaped to prevent XSS?
|
|
98
|
+
- Are SQL queries parameterized?
|
|
99
|
+
- Is CSRF protection enabled for state-changing endpoints?
|
|
100
|
+
|
|
101
|
+
### 4. Performance
|
|
102
|
+
|
|
103
|
+
- Any N+1 query patterns? (fetching in a loop instead of batching)
|
|
104
|
+
- Unbounded loops or recursion? (missing limits, no pagination)
|
|
105
|
+
- Missing database indexes for frequent queries?
|
|
106
|
+
- Unnecessary memory allocations? (large objects created in hot paths)
|
|
107
|
+
- Could any expensive operations be cached or deferred?
|
|
108
|
+
|
|
109
|
+
### 5. Readability
|
|
110
|
+
|
|
111
|
+
- Are names descriptive and intention-revealing?
|
|
112
|
+
- Are functions small and focused (under 50 lines)?
|
|
113
|
+
- Are files focused on a single concern (under 400 lines)?
|
|
114
|
+
- Is the nesting depth reasonable (4 levels or less)?
|
|
115
|
+
- Would a future developer understand this without asking the author?
|
|
116
|
+
|
|
117
|
+
### 6. Testing
|
|
118
|
+
|
|
119
|
+
- Do tests exist for all new behavior?
|
|
120
|
+
- Do existing tests still pass?
|
|
121
|
+
- Are edge cases tested (not just the happy path)?
|
|
122
|
+
- Are tests independent and deterministic (no flaky tests)?
|
|
123
|
+
- Is the test structure clear? (arrange-act-assert)
|
|
124
|
+
|
|
125
|
+
## Providing Feedback
|
|
126
|
+
|
|
127
|
+
### Use Severity Levels
|
|
128
|
+
|
|
129
|
+
Every review comment should be tagged with a severity so the author can prioritize:
|
|
130
|
+
|
|
131
|
+
- **CRITICAL** -- Must fix before merge. Bugs, security issues, data loss risks.
|
|
132
|
+
- **HIGH** -- Should fix before merge. Missing error handling, performance issues, incorrect behavior in edge cases.
|
|
133
|
+
- **MEDIUM** -- Consider fixing. Code quality improvements, better naming, minor refactoring opportunities.
|
|
134
|
+
- **LOW** -- Nit. Style preferences, optional improvements, suggestions for future work.
|
|
135
|
+
|
|
136
|
+
### Be Specific
|
|
137
|
+
|
|
138
|
+
Bad: "This is confusing."
|
|
139
|
+
Good: "The variable \`data\` on line 42 of user-service.ts does not convey what it holds. Consider renaming to \`activeUserRecords\` to match the query filter on line 38."
|
|
140
|
+
|
|
141
|
+
Every comment should include:
|
|
142
|
+
|
|
143
|
+
- The file and line (or line range)
|
|
144
|
+
- What the issue is
|
|
145
|
+
- A suggested fix or alternative approach
|
|
146
|
+
|
|
147
|
+
### Be Constructive
|
|
148
|
+
|
|
149
|
+
- Explain WHY something is a problem, not just WHAT is wrong
|
|
150
|
+
- Offer alternatives when pointing out issues
|
|
151
|
+
- Acknowledge good work -- positive feedback reinforces good patterns
|
|
152
|
+
- Use "we" language -- "We could improve this by..." not "You did this wrong"
|
|
153
|
+
- Ask questions when unsure -- "Is there a reason this is not using the existing helper?"
|
|
154
|
+
|
|
155
|
+
## Responding to Review Comments
|
|
156
|
+
|
|
157
|
+
### Address Every Comment
|
|
158
|
+
|
|
159
|
+
- Fix the issue, or explain why the current approach is intentional
|
|
160
|
+
- Never ignore a review comment without responding
|
|
161
|
+
- If you disagree, explain your reasoning -- the reviewer may have missed context
|
|
162
|
+
- If you agree but want to defer, create a follow-up issue and link it
|
|
163
|
+
|
|
164
|
+
### Stay Professional
|
|
165
|
+
|
|
166
|
+
- Do not take feedback personally -- reviews are about code, not about you
|
|
167
|
+
- Ask for clarification if a comment is unclear
|
|
168
|
+
- Thank reviewers for catching issues -- they saved you from a production bug
|
|
169
|
+
- If a discussion gets long, move to a synchronous conversation (call, pair session)
|
|
170
|
+
|
|
171
|
+
### Mark Resolved Comments
|
|
172
|
+
|
|
173
|
+
- After addressing a comment, mark it as resolved
|
|
174
|
+
- If the fix is in a follow-up commit, reference the commit hash
|
|
175
|
+
- Do not resolve comments that were not actually addressed
|
|
176
|
+
|
|
177
|
+
## Integration with Our Tools
|
|
178
|
+
|
|
179
|
+
### Automated Review with oc_review
|
|
180
|
+
|
|
181
|
+
Use \`oc_review\` for automated multi-agent code review. The review engine runs up to 21 specialist agents (universal + stack-gated) covering:
|
|
182
|
+
|
|
183
|
+
- Logic correctness and edge cases
|
|
184
|
+
- Security vulnerabilities and input validation
|
|
185
|
+
- Code quality and maintainability
|
|
186
|
+
- Testing completeness and test quality
|
|
187
|
+
- Performance and scalability concerns
|
|
188
|
+
- Documentation and naming
|
|
189
|
+
|
|
190
|
+
Automated review is a complement to human review, not a replacement. Use it for the mechanical checks so human reviewers can focus on architecture and design decisions.
|
|
191
|
+
|
|
192
|
+
### Coding Standards Baseline
|
|
193
|
+
|
|
194
|
+
Use the coding-standards skill as the shared baseline for quality checks. This ensures all reviewers apply the same standards for naming, file organization, error handling, immutability, and input validation.
|
|
195
|
+
|
|
196
|
+
### Review Workflow
|
|
197
|
+
|
|
198
|
+
The recommended workflow for any change:
|
|
199
|
+
|
|
200
|
+
1. Self-review the diff
|
|
201
|
+
2. Run \`oc_review\` for automated analysis
|
|
202
|
+
3. Address any CRITICAL or HIGH findings from automated review
|
|
203
|
+
4. Request human review with the context template above
|
|
204
|
+
5. Address human review feedback
|
|
205
|
+
6. Merge when all CRITICAL and HIGH items are resolved
|
|
206
|
+
|
|
207
|
+
## Anti-Pattern Catalog
|
|
208
|
+
|
|
209
|
+
### Anti-Pattern: Rubber-Stamp Reviews
|
|
210
|
+
|
|
211
|
+
**What it looks like:** Approving a PR after a cursory glance, or approving without reading the diff at all.
|
|
212
|
+
|
|
213
|
+
**Why it is harmful:** Defeats the entire purpose of code review. Bugs, security issues, and design problems ship to production uncaught.
|
|
214
|
+
|
|
215
|
+
**Instead:** Spend at least 10 minutes per 100 lines of meaningful diff. If you do not have time for a thorough review, say so and let someone else review.
|
|
216
|
+
|
|
217
|
+
### Anti-Pattern: Style-Only Reviews
|
|
218
|
+
|
|
219
|
+
**What it looks like:** Only commenting on formatting, whitespace, and naming conventions while ignoring logic, architecture, and security.
|
|
220
|
+
|
|
221
|
+
**Why it is harmful:** Misallocates review effort. Style issues are the least impactful category and can often be caught by linters.
|
|
222
|
+
|
|
223
|
+
**Instead:** Focus on correctness and architecture first (items 1-4 in the review order). Save style comments for LOW severity nits at the end.
|
|
224
|
+
|
|
225
|
+
### Anti-Pattern: Blocking on Nits
|
|
226
|
+
|
|
227
|
+
**What it looks like:** Requesting changes or withholding approval for trivial style preferences (single-line formatting, import order, comment wording).
|
|
228
|
+
|
|
229
|
+
**Why it is harmful:** Slows down delivery, creates frustration, and discourages submitting PRs. The cost of the delay exceeds the value of the nit fix.
|
|
230
|
+
|
|
231
|
+
**Instead:** Approve the PR with suggestions for LOW items. The author can address them in a follow-up or not -- it is their call.
|
|
232
|
+
|
|
233
|
+
### Anti-Pattern: Drive-By Reviews
|
|
234
|
+
|
|
235
|
+
**What it looks like:** Leaving a single comment on a large PR without reviewing the rest, giving the impression the PR was reviewed.
|
|
236
|
+
|
|
237
|
+
**Why it is harmful:** Creates false confidence that the code was reviewed when it was not.
|
|
238
|
+
|
|
239
|
+
**Instead:** If you only have time for a partial review, say so explicitly: "I only reviewed the auth changes, not the database migration. Someone else should review that part."
|
|
240
|
+
|
|
241
|
+
### Anti-Pattern: Review Ping-Pong
|
|
242
|
+
|
|
243
|
+
**What it looks like:** Reviewer leaves one comment, author fixes it, reviewer finds a new issue, author fixes that, ad infinitum.
|
|
244
|
+
|
|
245
|
+
**Why it is harmful:** Each round-trip adds latency. A thorough first review is faster than five rounds of incremental feedback.
|
|
246
|
+
|
|
247
|
+
**Instead:** Review the entire PR in one pass. Leave all comments at once. If you spot a pattern issue, note it once and add "same issue applies to lines X, Y, Z."
|
|
248
|
+
|
|
249
|
+
## Failure Modes
|
|
250
|
+
|
|
251
|
+
- **Review takes too long:** The PR is too large. Split it into smaller PRs.
|
|
252
|
+
- **Reviewer and author disagree:** Escalate to a tech lead or use an ADR (Architecture Decision Record) for design disagreements.
|
|
253
|
+
- **Same issues keep appearing:** The team needs better shared standards. Update the coding-standards skill or add linter rules.
|
|
254
|
+
- **Reviews feel adversarial:** Revisit the team's review culture. Reviews should feel collaborative, not combative.
|
|
255
|
+
</skill>
|
|
256
|
+
|
|
257
|
+
## Rules
|
|
258
|
+
|
|
259
|
+
- ALWAYS invoke oc_review when the user asks for a review. Do not perform manual review without the tool.
|
|
260
|
+
- Present findings organized by severity: CRITICAL first, then HIGH, MEDIUM, LOW.
|
|
261
|
+
- For each finding, include the file path, line range, issue description, and suggested fix.
|
|
262
|
+
- NEVER apply fixes yourself -- you review, you do not fix. Tell the user what to fix.
|
|
263
|
+
- NEVER approve code without running oc_review first.
|
|
264
|
+
- You are distinct from the pr-reviewer agent. You review code changes (staged, unstaged, or between branches). The pr-reviewer handles GitHub PR-specific workflows.`,
|
|
265
|
+
permission: {
|
|
266
|
+
edit: "deny",
|
|
267
|
+
bash: "allow",
|
|
268
|
+
webfetch: "deny",
|
|
269
|
+
} as const,
|
|
270
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fallbackConfigSchema,
|
|
7
|
+
fallbackConfigSchemaV6,
|
|
8
|
+
fallbackDefaults,
|
|
9
|
+
fallbackDefaultsV6,
|
|
10
|
+
testModeDefaults,
|
|
11
|
+
} from "./orchestrator/fallback/fallback-config";
|
|
6
12
|
import { AGENT_REGISTRY, ALL_GROUP_IDS } from "./registry/model-groups";
|
|
7
13
|
import { ensureDir, isEnoentError } from "./utils/fs-helpers";
|
|
8
14
|
import { getGlobalConfigDir } from "./utils/paths";
|
|
@@ -149,10 +155,37 @@ const pluginConfigSchemaV5 = z
|
|
|
149
155
|
}
|
|
150
156
|
});
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
export const pluginConfigSchema = pluginConfigSchemaV5;
|
|
158
|
+
type PluginConfigV5 = z.infer<typeof pluginConfigSchemaV5>;
|
|
154
159
|
|
|
155
|
-
|
|
160
|
+
// --- V6 schema ---
|
|
161
|
+
|
|
162
|
+
const pluginConfigSchemaV6 = z
|
|
163
|
+
.object({
|
|
164
|
+
version: z.literal(6),
|
|
165
|
+
configured: z.boolean(),
|
|
166
|
+
groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
|
|
167
|
+
overrides: z.record(z.string(), agentOverrideSchema).default({}),
|
|
168
|
+
orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
|
|
169
|
+
confidence: confidenceConfigSchema.default(confidenceDefaults),
|
|
170
|
+
fallback: fallbackConfigSchemaV6.default(fallbackDefaultsV6),
|
|
171
|
+
memory: memoryConfigSchema.default(memoryDefaults),
|
|
172
|
+
})
|
|
173
|
+
.superRefine((config, ctx) => {
|
|
174
|
+
for (const groupId of Object.keys(config.groups)) {
|
|
175
|
+
if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
|
|
176
|
+
ctx.addIssue({
|
|
177
|
+
code: z.ZodIssueCode.custom,
|
|
178
|
+
path: ["groups", groupId],
|
|
179
|
+
message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Export aliases updated to v6
|
|
186
|
+
export const pluginConfigSchema = pluginConfigSchemaV6;
|
|
187
|
+
|
|
188
|
+
export type PluginConfig = z.infer<typeof pluginConfigSchemaV6>;
|
|
156
189
|
|
|
157
190
|
export const CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
|
|
158
191
|
|
|
@@ -228,7 +261,7 @@ function migrateV3toV4(v3Config: PluginConfigV3): PluginConfigV4 {
|
|
|
228
261
|
};
|
|
229
262
|
}
|
|
230
263
|
|
|
231
|
-
function migrateV4toV5(v4Config: PluginConfigV4):
|
|
264
|
+
function migrateV4toV5(v4Config: PluginConfigV4): PluginConfigV5 {
|
|
232
265
|
return {
|
|
233
266
|
version: 5 as const,
|
|
234
267
|
configured: v4Config.configured,
|
|
@@ -241,6 +274,19 @@ function migrateV4toV5(v4Config: PluginConfigV4): PluginConfig {
|
|
|
241
274
|
};
|
|
242
275
|
}
|
|
243
276
|
|
|
277
|
+
function migrateV5toV6(v5Config: PluginConfigV5): PluginConfig {
|
|
278
|
+
return {
|
|
279
|
+
version: 6 as const,
|
|
280
|
+
configured: v5Config.configured,
|
|
281
|
+
groups: v5Config.groups,
|
|
282
|
+
overrides: v5Config.overrides,
|
|
283
|
+
orchestrator: v5Config.orchestrator,
|
|
284
|
+
confidence: v5Config.confidence,
|
|
285
|
+
fallback: { ...v5Config.fallback, testMode: testModeDefaults },
|
|
286
|
+
memory: v5Config.memory,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
244
290
|
// --- Public API ---
|
|
245
291
|
|
|
246
292
|
export async function loadConfig(configPath: string = CONFIG_PATH): Promise<PluginConfig | null> {
|
|
@@ -248,49 +294,61 @@ export async function loadConfig(configPath: string = CONFIG_PATH): Promise<Plug
|
|
|
248
294
|
const raw = await readFile(configPath, "utf-8");
|
|
249
295
|
const parsed = JSON.parse(raw);
|
|
250
296
|
|
|
251
|
-
// Try
|
|
297
|
+
// Try v6 first
|
|
298
|
+
const v6Result = pluginConfigSchemaV6.safeParse(parsed);
|
|
299
|
+
if (v6Result.success) return v6Result.data;
|
|
300
|
+
|
|
301
|
+
// Try v5 and migrate to v6
|
|
252
302
|
const v5Result = pluginConfigSchemaV5.safeParse(parsed);
|
|
253
|
-
if (v5Result.success)
|
|
303
|
+
if (v5Result.success) {
|
|
304
|
+
const migrated = migrateV5toV6(v5Result.data);
|
|
305
|
+
await saveConfig(migrated, configPath);
|
|
306
|
+
return migrated;
|
|
307
|
+
}
|
|
254
308
|
|
|
255
|
-
// Try v4
|
|
309
|
+
// Try v4 → v5 → v6
|
|
256
310
|
const v4Result = pluginConfigSchemaV4.safeParse(parsed);
|
|
257
311
|
if (v4Result.success) {
|
|
258
|
-
const
|
|
312
|
+
const v5 = migrateV4toV5(v4Result.data);
|
|
313
|
+
const migrated = migrateV5toV6(v5);
|
|
259
314
|
await saveConfig(migrated, configPath);
|
|
260
315
|
return migrated;
|
|
261
316
|
}
|
|
262
317
|
|
|
263
|
-
// Try v3 → v4 → v5
|
|
318
|
+
// Try v3 → v4 → v5 → v6
|
|
264
319
|
const v3Result = pluginConfigSchemaV3.safeParse(parsed);
|
|
265
320
|
if (v3Result.success) {
|
|
266
321
|
const v4 = migrateV3toV4(v3Result.data);
|
|
267
|
-
const
|
|
322
|
+
const v5 = migrateV4toV5(v4);
|
|
323
|
+
const migrated = migrateV5toV6(v5);
|
|
268
324
|
await saveConfig(migrated, configPath);
|
|
269
325
|
return migrated;
|
|
270
326
|
}
|
|
271
327
|
|
|
272
|
-
// Try v2 → v3 → v4 → v5
|
|
328
|
+
// Try v2 → v3 → v4 → v5 → v6
|
|
273
329
|
const v2Result = pluginConfigSchemaV2.safeParse(parsed);
|
|
274
330
|
if (v2Result.success) {
|
|
275
331
|
const v3 = migrateV2toV3(v2Result.data);
|
|
276
332
|
const v4 = migrateV3toV4(v3);
|
|
277
|
-
const
|
|
333
|
+
const v5 = migrateV4toV5(v4);
|
|
334
|
+
const migrated = migrateV5toV6(v5);
|
|
278
335
|
await saveConfig(migrated, configPath);
|
|
279
336
|
return migrated;
|
|
280
337
|
}
|
|
281
338
|
|
|
282
|
-
// Try v1 → v2 → v3 → v4 → v5
|
|
339
|
+
// Try v1 → v2 → v3 → v4 → v5 → v6
|
|
283
340
|
const v1Result = pluginConfigSchemaV1.safeParse(parsed);
|
|
284
341
|
if (v1Result.success) {
|
|
285
342
|
const v2 = migrateV1toV2(v1Result.data);
|
|
286
343
|
const v3 = migrateV2toV3(v2);
|
|
287
344
|
const v4 = migrateV3toV4(v3);
|
|
288
|
-
const
|
|
345
|
+
const v5 = migrateV4toV5(v4);
|
|
346
|
+
const migrated = migrateV5toV6(v5);
|
|
289
347
|
await saveConfig(migrated, configPath);
|
|
290
348
|
return migrated;
|
|
291
349
|
}
|
|
292
350
|
|
|
293
|
-
return
|
|
351
|
+
return pluginConfigSchemaV6.parse(parsed); // throw with proper error
|
|
294
352
|
} catch (error: unknown) {
|
|
295
353
|
if (isEnoentError(error)) return null;
|
|
296
354
|
throw error;
|
|
@@ -313,13 +371,13 @@ export function isFirstLoad(config: PluginConfig | null): boolean {
|
|
|
313
371
|
|
|
314
372
|
export function createDefaultConfig(): PluginConfig {
|
|
315
373
|
return {
|
|
316
|
-
version:
|
|
374
|
+
version: 6 as const,
|
|
317
375
|
configured: false,
|
|
318
376
|
groups: {},
|
|
319
377
|
overrides: {},
|
|
320
378
|
orchestrator: orchestratorDefaults,
|
|
321
379
|
confidence: confidenceDefaults,
|
|
322
|
-
fallback:
|
|
380
|
+
fallback: fallbackDefaultsV6,
|
|
323
381
|
memory: memoryDefaults,
|
|
324
382
|
};
|
|
325
383
|
}
|
package/src/health/checks.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import type { Config } from "@opencode-ai/plugin";
|
|
5
|
+
import { parse } from "yaml";
|
|
3
6
|
import { loadConfig } from "../config";
|
|
7
|
+
import { DB_FILE, MEMORY_DIR } from "../memory/constants";
|
|
4
8
|
import { AGENT_NAMES } from "../orchestrator/handlers/types";
|
|
9
|
+
import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
|
|
10
|
+
import { loadAllSkills } from "../skills/loader";
|
|
5
11
|
import { getAssetsDir, getGlobalConfigDir } from "../utils/paths";
|
|
6
12
|
import type { HealthResult } from "./types";
|
|
7
13
|
|
|
@@ -123,3 +129,178 @@ export async function assetHealthCheck(
|
|
|
123
129
|
});
|
|
124
130
|
}
|
|
125
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check skill loading status per detected project stack.
|
|
135
|
+
* Reports which stacks are detected and how many skills match.
|
|
136
|
+
* Accepts optional skillsDir for testability (defaults to global config skills dir).
|
|
137
|
+
*/
|
|
138
|
+
export async function skillHealthCheck(
|
|
139
|
+
projectRoot: string,
|
|
140
|
+
skillsDir?: string,
|
|
141
|
+
): Promise<HealthResult> {
|
|
142
|
+
try {
|
|
143
|
+
const tags = await detectProjectStackTags(projectRoot);
|
|
144
|
+
const resolvedSkillsDir = skillsDir ?? join(getGlobalConfigDir(), "skills");
|
|
145
|
+
const allSkills = await loadAllSkills(resolvedSkillsDir);
|
|
146
|
+
const filtered = filterSkillsByStack(allSkills, tags);
|
|
147
|
+
|
|
148
|
+
const stackLabel = tags.length > 0 ? tags.join(", ") : "none";
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
name: "skill-loading",
|
|
151
|
+
status: "pass" as const,
|
|
152
|
+
message: `Detected stacks: [${stackLabel}], ${filtered.size}/${allSkills.size} skills matched`,
|
|
153
|
+
details: Object.freeze([...filtered.keys()]),
|
|
154
|
+
});
|
|
155
|
+
} catch (error: unknown) {
|
|
156
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
name: "skill-loading",
|
|
159
|
+
status: "fail" as const,
|
|
160
|
+
message: `Skill check failed: ${msg}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check memory DB health: existence, readability, observation count.
|
|
167
|
+
* Does NOT call getMemoryDb() to avoid creating an empty DB as a side effect.
|
|
168
|
+
* Uses readonly DB access for safe inspection.
|
|
169
|
+
* Accepts optional baseDir for testability (defaults to global config dir).
|
|
170
|
+
*/
|
|
171
|
+
export async function memoryHealthCheck(baseDir?: string): Promise<HealthResult> {
|
|
172
|
+
const resolvedBase = baseDir ?? getGlobalConfigDir();
|
|
173
|
+
const dbPath = join(resolvedBase, MEMORY_DIR, DB_FILE);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await access(dbPath);
|
|
177
|
+
} catch (error: unknown) {
|
|
178
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
179
|
+
if (code === "ENOENT") {
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
name: "memory-db",
|
|
182
|
+
status: "pass" as const,
|
|
183
|
+
message: `Memory DB not yet initialized -- will be created on first memory capture`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
name: "memory-db",
|
|
189
|
+
status: "fail" as const,
|
|
190
|
+
message: `Memory DB inaccessible: ${msg}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const fileStat = await stat(dbPath);
|
|
196
|
+
const sizeKB = (fileStat.size / 1024).toFixed(1);
|
|
197
|
+
|
|
198
|
+
if (fileStat.size === 0) {
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
name: "memory-db",
|
|
201
|
+
status: "fail" as const,
|
|
202
|
+
message: `Memory DB exists but is empty (0 bytes)`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const db = new Database(dbPath, { readonly: true });
|
|
207
|
+
try {
|
|
208
|
+
const row = db.query("SELECT COUNT(*) as count FROM observations").get() as {
|
|
209
|
+
count: number;
|
|
210
|
+
} | null;
|
|
211
|
+
const count = row?.count ?? 0;
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
name: "memory-db",
|
|
214
|
+
status: "pass" as const,
|
|
215
|
+
message: `Memory DB exists (${count} observation${count !== 1 ? "s" : ""}, ${sizeKB}KB)`,
|
|
216
|
+
});
|
|
217
|
+
} finally {
|
|
218
|
+
db.close();
|
|
219
|
+
}
|
|
220
|
+
} catch (error: unknown) {
|
|
221
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
name: "memory-db",
|
|
224
|
+
status: "fail" as const,
|
|
225
|
+
message: `Memory DB read error: ${msg}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Expected command files that should exist in the commands directory. */
|
|
231
|
+
const EXPECTED_COMMANDS: readonly string[] = Object.freeze([
|
|
232
|
+
"oc-tdd",
|
|
233
|
+
"oc-review-pr",
|
|
234
|
+
"oc-brainstorm",
|
|
235
|
+
"oc-write-plan",
|
|
236
|
+
"oc-stocktake",
|
|
237
|
+
"oc-update-docs",
|
|
238
|
+
"oc-new-agent",
|
|
239
|
+
"oc-new-skill",
|
|
240
|
+
"oc-new-command",
|
|
241
|
+
"oc-quick",
|
|
242
|
+
"oc-review-agents",
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check command accessibility: file existence and valid YAML frontmatter.
|
|
247
|
+
* Verifies each expected command file exists and has a non-empty description.
|
|
248
|
+
*/
|
|
249
|
+
export async function commandHealthCheck(targetDir?: string): Promise<HealthResult> {
|
|
250
|
+
const dir = targetDir ?? getGlobalConfigDir();
|
|
251
|
+
const missing: string[] = [];
|
|
252
|
+
const invalid: string[] = [];
|
|
253
|
+
|
|
254
|
+
await Promise.all(
|
|
255
|
+
EXPECTED_COMMANDS.map(async (name) => {
|
|
256
|
+
const filePath = join(dir, "commands", `${name}.md`);
|
|
257
|
+
try {
|
|
258
|
+
const content = await readFile(filePath, "utf-8");
|
|
259
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
260
|
+
if (!fmMatch) {
|
|
261
|
+
invalid.push(`${name}: no frontmatter`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const parsed = parse(fmMatch[1]);
|
|
266
|
+
if (
|
|
267
|
+
parsed === null ||
|
|
268
|
+
typeof parsed !== "object" ||
|
|
269
|
+
typeof parsed.description !== "string" ||
|
|
270
|
+
parsed.description.trim().length === 0
|
|
271
|
+
) {
|
|
272
|
+
invalid.push(`${name}: missing or empty description`);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
invalid.push(`${name}: invalid YAML frontmatter`);
|
|
276
|
+
}
|
|
277
|
+
} catch (error: unknown) {
|
|
278
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
279
|
+
if (code === "ENOENT") {
|
|
280
|
+
missing.push(name);
|
|
281
|
+
} else {
|
|
282
|
+
invalid.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
missing.sort();
|
|
289
|
+
invalid.sort();
|
|
290
|
+
const issues = [...missing.map((n) => `missing: ${n}`), ...invalid];
|
|
291
|
+
|
|
292
|
+
if (issues.length === 0) {
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
name: "command-accessibility",
|
|
295
|
+
status: "pass" as const,
|
|
296
|
+
message: `All ${EXPECTED_COMMANDS.length} commands accessible`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
name: "command-accessibility",
|
|
302
|
+
status: "fail" as const,
|
|
303
|
+
message: `${issues.length} command issue(s) found`,
|
|
304
|
+
details: Object.freeze(issues),
|
|
305
|
+
});
|
|
306
|
+
}
|
package/src/health/runner.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Config } from "@opencode-ai/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
agentHealthCheck,
|
|
4
|
+
assetHealthCheck,
|
|
5
|
+
commandHealthCheck,
|
|
6
|
+
configHealthCheck,
|
|
7
|
+
memoryHealthCheck,
|
|
8
|
+
skillHealthCheck,
|
|
9
|
+
} from "./checks";
|
|
3
10
|
import type { HealthReport, HealthResult } from "./types";
|
|
4
11
|
|
|
5
12
|
/**
|
|
@@ -31,6 +38,7 @@ export async function runHealthChecks(options?: {
|
|
|
31
38
|
openCodeConfig?: Config | null;
|
|
32
39
|
assetsDir?: string;
|
|
33
40
|
targetDir?: string;
|
|
41
|
+
projectRoot?: string;
|
|
34
42
|
}): Promise<HealthReport> {
|
|
35
43
|
const start = Date.now();
|
|
36
44
|
|
|
@@ -38,9 +46,19 @@ export async function runHealthChecks(options?: {
|
|
|
38
46
|
configHealthCheck(options?.configPath),
|
|
39
47
|
agentHealthCheck(options?.openCodeConfig ?? null),
|
|
40
48
|
assetHealthCheck(options?.assetsDir, options?.targetDir),
|
|
49
|
+
skillHealthCheck(options?.projectRoot ?? process.cwd()),
|
|
50
|
+
memoryHealthCheck(options?.targetDir),
|
|
51
|
+
commandHealthCheck(options?.targetDir),
|
|
41
52
|
]);
|
|
42
53
|
|
|
43
|
-
const fallbackNames = [
|
|
54
|
+
const fallbackNames = [
|
|
55
|
+
"config-validity",
|
|
56
|
+
"agent-injection",
|
|
57
|
+
"asset-directories",
|
|
58
|
+
"skill-loading",
|
|
59
|
+
"memory-db",
|
|
60
|
+
"command-accessibility",
|
|
61
|
+
];
|
|
44
62
|
const results: readonly HealthResult[] = Object.freeze(
|
|
45
63
|
settled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
|
|
46
64
|
);
|