@kodrunhq/opencode-autopilot 1.4.0 → 1.6.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/brainstorm.md +7 -0
- package/assets/commands/stocktake.md +7 -0
- package/assets/commands/tdd.md +7 -0
- package/assets/commands/update-docs.md +7 -0
- package/assets/commands/write-plan.md +7 -0
- package/assets/skills/brainstorming/SKILL.md +295 -0
- package/assets/skills/code-review/SKILL.md +241 -0
- package/assets/skills/e2e-testing/SKILL.md +266 -0
- package/assets/skills/git-worktrees/SKILL.md +296 -0
- package/assets/skills/go-patterns/SKILL.md +240 -0
- package/assets/skills/plan-executing/SKILL.md +258 -0
- package/assets/skills/plan-writing/SKILL.md +278 -0
- package/assets/skills/python-patterns/SKILL.md +255 -0
- package/assets/skills/rust-patterns/SKILL.md +293 -0
- package/assets/skills/strategic-compaction/SKILL.md +217 -0
- package/assets/skills/systematic-debugging/SKILL.md +299 -0
- package/assets/skills/tdd-workflow/SKILL.md +311 -0
- package/assets/skills/typescript-patterns/SKILL.md +278 -0
- package/assets/skills/verification/SKILL.md +240 -0
- package/bin/configure-tui.ts +1 -1
- package/package.json +1 -1
- package/src/config.ts +76 -14
- package/src/index.ts +43 -2
- package/src/memory/capture.ts +205 -0
- package/src/memory/constants.ts +26 -0
- package/src/memory/database.ts +103 -0
- package/src/memory/decay.ts +94 -0
- package/src/memory/index.ts +24 -0
- package/src/memory/injector.ts +85 -0
- package/src/memory/project-key.ts +5 -0
- package/src/memory/repository.ts +217 -0
- package/src/memory/retrieval.ts +260 -0
- package/src/memory/schemas.ts +34 -0
- package/src/memory/types.ts +12 -0
- package/src/orchestrator/skill-injection.ts +38 -0
- package/src/review/sanitize.ts +1 -1
- package/src/skills/adaptive-injector.ts +122 -0
- package/src/skills/dependency-resolver.ts +88 -0
- package/src/skills/linter.ts +113 -0
- package/src/skills/loader.ts +88 -0
- package/src/templates/skill-template.ts +4 -0
- package/src/tools/configure.ts +1 -1
- package/src/tools/create-skill.ts +12 -0
- package/src/tools/memory-status.ts +164 -0
- package/src/tools/stocktake.ts +170 -0
- package/src/tools/update-docs.ts +116 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import { lintAgent, lintCommand, lintSkill } from "../skills/linter";
|
|
5
|
+
import { getAssetsDir, getGlobalConfigDir } from "../utils/paths";
|
|
6
|
+
|
|
7
|
+
interface StocktakeArgs {
|
|
8
|
+
readonly lint?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface AssetEntry {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly type: "skill" | "command" | "agent";
|
|
14
|
+
readonly origin: "built-in" | "user-created";
|
|
15
|
+
readonly lint?: {
|
|
16
|
+
readonly valid: boolean;
|
|
17
|
+
readonly errors: readonly string[];
|
|
18
|
+
readonly warnings: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read directory entries safely, returning empty array on ENOENT only. */
|
|
23
|
+
async function safeReaddir(dirPath: string): Promise<string[]> {
|
|
24
|
+
try {
|
|
25
|
+
return await readdir(dirPath);
|
|
26
|
+
} catch (error: unknown) {
|
|
27
|
+
const errObj = error as { code?: unknown };
|
|
28
|
+
if (errObj?.code === "ENOENT") return [];
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Cache for built-in asset lookups (keyed by assetType). */
|
|
34
|
+
const builtInCache = new Map<string, ReadonlySet<string>>();
|
|
35
|
+
|
|
36
|
+
/** Check if an asset name exists in the bundled assets directory. */
|
|
37
|
+
async function isBuiltIn(assetType: string, name: string): Promise<boolean> {
|
|
38
|
+
let cached = builtInCache.get(assetType);
|
|
39
|
+
if (!cached) {
|
|
40
|
+
const assetsDir = getAssetsDir();
|
|
41
|
+
const entries = await safeReaddir(join(assetsDir, assetType));
|
|
42
|
+
cached = new Set(entries);
|
|
43
|
+
builtInCache.set(assetType, cached);
|
|
44
|
+
}
|
|
45
|
+
return cached.has(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function stocktakeCore(args: StocktakeArgs, baseDir: string): Promise<string> {
|
|
49
|
+
const shouldLint = args.lint !== false;
|
|
50
|
+
const skills: AssetEntry[] = [];
|
|
51
|
+
const commands: AssetEntry[] = [];
|
|
52
|
+
const agents: AssetEntry[] = [];
|
|
53
|
+
|
|
54
|
+
// Scan skills (each subdirectory is a skill) — filter to directories only
|
|
55
|
+
const skillEntries = await readdir(join(baseDir, "skills"), { withFileTypes: true }).catch(
|
|
56
|
+
() => [],
|
|
57
|
+
);
|
|
58
|
+
const skillDirs = skillEntries
|
|
59
|
+
.filter((e) => e.isDirectory() && e.name !== ".gitkeep")
|
|
60
|
+
.map((e) => e.name);
|
|
61
|
+
for (const name of skillDirs) {
|
|
62
|
+
const skillFile = join(baseDir, "skills", name, "SKILL.md");
|
|
63
|
+
const origin = (await isBuiltIn("skills", name)) ? "built-in" : "user-created";
|
|
64
|
+
const entry: AssetEntry = { name, type: "skill", origin };
|
|
65
|
+
|
|
66
|
+
if (shouldLint) {
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(skillFile, "utf-8");
|
|
69
|
+
const lint = lintSkill(content);
|
|
70
|
+
skills.push({ ...entry, lint });
|
|
71
|
+
} catch {
|
|
72
|
+
skills.push({
|
|
73
|
+
...entry,
|
|
74
|
+
lint: { valid: false, errors: ["Could not read SKILL.md"], warnings: [] },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
skills.push(entry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Scan commands (.md files)
|
|
83
|
+
const commandFiles = await safeReaddir(join(baseDir, "commands"));
|
|
84
|
+
for (const file of commandFiles.filter((f) => f.endsWith(".md"))) {
|
|
85
|
+
const name = file.replace(/\.md$/, "");
|
|
86
|
+
const origin = (await isBuiltIn("commands", file)) ? "built-in" : "user-created";
|
|
87
|
+
const entry: AssetEntry = { name, type: "command", origin };
|
|
88
|
+
|
|
89
|
+
if (shouldLint) {
|
|
90
|
+
try {
|
|
91
|
+
const content = await readFile(join(baseDir, "commands", file), "utf-8");
|
|
92
|
+
const lint = lintCommand(content);
|
|
93
|
+
commands.push({ ...entry, lint });
|
|
94
|
+
} catch {
|
|
95
|
+
commands.push({
|
|
96
|
+
...entry,
|
|
97
|
+
lint: { valid: false, errors: ["Could not read command file"], warnings: [] },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
commands.push(entry);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Scan agents (.md files)
|
|
106
|
+
const agentFiles = await safeReaddir(join(baseDir, "agents"));
|
|
107
|
+
for (const file of agentFiles.filter((f) => f.endsWith(".md"))) {
|
|
108
|
+
const name = file.replace(/\.md$/, "");
|
|
109
|
+
const origin = (await isBuiltIn("agents", file)) ? "built-in" : "user-created";
|
|
110
|
+
const entry: AssetEntry = { name, type: "agent", origin };
|
|
111
|
+
|
|
112
|
+
if (shouldLint) {
|
|
113
|
+
try {
|
|
114
|
+
const content = await readFile(join(baseDir, "agents", file), "utf-8");
|
|
115
|
+
const lint = lintAgent(content);
|
|
116
|
+
agents.push({ ...entry, lint });
|
|
117
|
+
} catch {
|
|
118
|
+
agents.push({
|
|
119
|
+
...entry,
|
|
120
|
+
lint: { valid: false, errors: ["Could not read agent file"], warnings: [] },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
agents.push(entry);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Compute summary
|
|
129
|
+
const allAssets = [...skills, ...commands, ...agents];
|
|
130
|
+
const builtIn = allAssets.filter((a) => a.origin === "built-in").length;
|
|
131
|
+
const userCreated = allAssets.filter((a) => a.origin === "user-created").length;
|
|
132
|
+
const lintErrors = shouldLint
|
|
133
|
+
? allAssets.reduce((sum, a) => sum + (a.lint?.errors.length ?? 0), 0)
|
|
134
|
+
: 0;
|
|
135
|
+
const lintWarnings = shouldLint
|
|
136
|
+
? allAssets.reduce((sum, a) => sum + (a.lint?.warnings.length ?? 0), 0)
|
|
137
|
+
: 0;
|
|
138
|
+
|
|
139
|
+
return JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
skills,
|
|
142
|
+
commands,
|
|
143
|
+
agents,
|
|
144
|
+
summary: {
|
|
145
|
+
total: allAssets.length,
|
|
146
|
+
builtIn,
|
|
147
|
+
userCreated,
|
|
148
|
+
lintErrors,
|
|
149
|
+
lintWarnings,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
2,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const ocStocktake = tool({
|
|
158
|
+
description:
|
|
159
|
+
"Audit all installed skills, commands, and agents with optional YAML frontmatter lint validation.",
|
|
160
|
+
args: {
|
|
161
|
+
lint: tool.schema
|
|
162
|
+
.boolean()
|
|
163
|
+
.optional()
|
|
164
|
+
.default(true)
|
|
165
|
+
.describe("Run YAML frontmatter linter on all assets"),
|
|
166
|
+
},
|
|
167
|
+
async execute(args) {
|
|
168
|
+
return stocktakeCore(args, getGlobalConfigDir());
|
|
169
|
+
},
|
|
170
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
|
|
6
|
+
const execFile = promisify(execFileCb);
|
|
7
|
+
|
|
8
|
+
interface UpdateDocsArgs {
|
|
9
|
+
readonly scope?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AffectedDoc {
|
|
13
|
+
readonly doc: string;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
readonly suggestion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Run a git command and return stdout lines (empty array on error). */
|
|
19
|
+
async function gitLines(args: readonly string[], cwd: string): Promise<readonly string[]> {
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execFile("git", [...args], { cwd });
|
|
22
|
+
return stdout
|
|
23
|
+
.trim()
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter((line) => line.length > 0);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function updateDocsCore(args: UpdateDocsArgs, projectDir: string): Promise<string> {
|
|
32
|
+
const scope = args.scope ?? "changed";
|
|
33
|
+
|
|
34
|
+
// Get changed source files
|
|
35
|
+
const changedFiles: readonly string[] =
|
|
36
|
+
scope === "all"
|
|
37
|
+
? await gitLines(["ls-files"], projectDir)
|
|
38
|
+
: await gitLines(["diff", "--name-only", "HEAD"], projectDir);
|
|
39
|
+
|
|
40
|
+
// Get all markdown files in the project
|
|
41
|
+
const mdFiles = await gitLines(["ls-files", "*.md"], projectDir);
|
|
42
|
+
|
|
43
|
+
// For each changed source file, check if any markdown file references it
|
|
44
|
+
const affectedDocs: AffectedDoc[] = [];
|
|
45
|
+
const seenDocs = new Set<string>();
|
|
46
|
+
|
|
47
|
+
for (const changedFile of changedFiles) {
|
|
48
|
+
// Skip markdown files themselves
|
|
49
|
+
if (changedFile.endsWith(".md")) continue;
|
|
50
|
+
|
|
51
|
+
const fileBaseName = basename(changedFile);
|
|
52
|
+
// Strip extension for module-style references
|
|
53
|
+
const moduleName = fileBaseName.replace(/\.[^.]+$/, "");
|
|
54
|
+
|
|
55
|
+
for (const mdFile of mdFiles) {
|
|
56
|
+
if (seenDocs.has(`${mdFile}:${changedFile}`)) continue;
|
|
57
|
+
|
|
58
|
+
// Simple heuristic: check if the markdown file path suggests it documents this area
|
|
59
|
+
// or if the changed file's name/module appears in common documentation patterns
|
|
60
|
+
const mdBaseName = basename(mdFile).replace(/\.md$/, "").toLowerCase();
|
|
61
|
+
const changedDir = changedFile.split("/").slice(0, -1).join("/");
|
|
62
|
+
|
|
63
|
+
const isRelated =
|
|
64
|
+
mdBaseName === "readme" ||
|
|
65
|
+
mdBaseName === moduleName.toLowerCase() ||
|
|
66
|
+
(changedDir.length > 0 && mdFile.toLowerCase().includes(changedDir.toLowerCase()));
|
|
67
|
+
|
|
68
|
+
if (isRelated) {
|
|
69
|
+
seenDocs.add(`${mdFile}:${changedFile}`);
|
|
70
|
+
affectedDocs.push({
|
|
71
|
+
doc: mdFile,
|
|
72
|
+
reason: `may be related to ${changedFile} (heuristic: path/name match)`,
|
|
73
|
+
suggestion: `Review ${mdFile} — it may need updates to reflect changes to ${changedFile}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deduplicate by doc path
|
|
80
|
+
const uniqueDocs = Array.from(
|
|
81
|
+
affectedDocs
|
|
82
|
+
.reduce((map, item) => {
|
|
83
|
+
if (!map.has(item.doc)) {
|
|
84
|
+
map.set(item.doc, item);
|
|
85
|
+
}
|
|
86
|
+
return map;
|
|
87
|
+
}, new Map<string, AffectedDoc>())
|
|
88
|
+
.values(),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const nonMdChanged = changedFiles.filter((f) => !f.endsWith(".md"));
|
|
92
|
+
|
|
93
|
+
return JSON.stringify(
|
|
94
|
+
{
|
|
95
|
+
changedFiles: nonMdChanged,
|
|
96
|
+
affectedDocs: uniqueDocs,
|
|
97
|
+
summary: `${nonMdChanged.length} source files changed, ${uniqueDocs.length} docs may need updates`,
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const ocUpdateDocs = tool({
|
|
105
|
+
description: "Detect documentation affected by recent code changes and suggest updates.",
|
|
106
|
+
args: {
|
|
107
|
+
scope: tool.schema
|
|
108
|
+
.enum(["changed", "all"])
|
|
109
|
+
.optional()
|
|
110
|
+
.default("changed")
|
|
111
|
+
.describe("Scope: 'changed' for git diff, 'all' for full scan"),
|
|
112
|
+
},
|
|
113
|
+
async execute(args) {
|
|
114
|
+
return updateDocsCore(args, process.cwd());
|
|
115
|
+
},
|
|
116
|
+
});
|