locus-product-planning 1.0.0 → 1.1.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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +32 -0
- package/README.md +127 -45
- package/agents/engineering/architect-reviewer.md +122 -0
- package/agents/engineering/engineering-manager.md +101 -0
- package/agents/engineering/principal-engineer.md +98 -0
- package/agents/engineering/staff-engineer.md +86 -0
- package/agents/engineering/tech-lead.md +114 -0
- package/agents/executive/ceo-strategist.md +81 -0
- package/agents/executive/cfo-analyst.md +97 -0
- package/agents/executive/coo-operations.md +100 -0
- package/agents/executive/cpo-product.md +104 -0
- package/agents/executive/cto-architect.md +90 -0
- package/agents/product/product-manager.md +70 -0
- package/agents/product/project-manager.md +95 -0
- package/agents/product/qa-strategist.md +132 -0
- package/agents/product/scrum-master.md +70 -0
- package/dist/index.d.ts +10 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -95
- package/dist/lib/skills-core.d.ts +95 -0
- package/dist/lib/skills-core.d.ts.map +1 -0
- package/dist/lib/skills-core.js +361 -0
- package/hooks/hooks.json +15 -0
- package/hooks/run-hook.cmd +32 -0
- package/hooks/session-start.cmd +13 -0
- package/hooks/session-start.sh +70 -0
- package/opencode.json +11 -7
- package/package.json +18 -4
- package/skills/01-executive-suite/ceo-strategist/SKILL.md +132 -0
- package/skills/01-executive-suite/cfo-analyst/SKILL.md +187 -0
- package/skills/01-executive-suite/coo-operations/SKILL.md +211 -0
- package/skills/01-executive-suite/cpo-product/SKILL.md +231 -0
- package/skills/01-executive-suite/cto-architect/SKILL.md +173 -0
- package/skills/02-product-management/estimation-expert/SKILL.md +139 -0
- package/skills/02-product-management/product-manager/SKILL.md +265 -0
- package/skills/02-product-management/program-manager/SKILL.md +178 -0
- package/skills/02-product-management/project-manager/SKILL.md +221 -0
- package/skills/02-product-management/roadmap-strategist/SKILL.md +186 -0
- package/skills/02-product-management/scrum-master/SKILL.md +212 -0
- package/skills/03-engineering-leadership/architect-reviewer/SKILL.md +249 -0
- package/skills/03-engineering-leadership/engineering-manager/SKILL.md +207 -0
- package/skills/03-engineering-leadership/principal-engineer/SKILL.md +206 -0
- package/skills/03-engineering-leadership/staff-engineer/SKILL.md +237 -0
- package/skills/03-engineering-leadership/tech-lead/SKILL.md +296 -0
- package/skills/04-developer-specializations/core/backend-developer/SKILL.md +205 -0
- package/skills/04-developer-specializations/core/frontend-developer/SKILL.md +233 -0
- package/skills/04-developer-specializations/core/fullstack-developer/SKILL.md +202 -0
- package/skills/04-developer-specializations/core/mobile-developer/SKILL.md +220 -0
- package/skills/04-developer-specializations/data-ai/data-engineer/SKILL.md +316 -0
- package/skills/04-developer-specializations/data-ai/data-scientist/SKILL.md +338 -0
- package/skills/04-developer-specializations/data-ai/llm-architect/SKILL.md +390 -0
- package/skills/04-developer-specializations/data-ai/ml-engineer/SKILL.md +349 -0
- package/skills/04-developer-specializations/infrastructure/cloud-architect/SKILL.md +354 -0
- package/skills/04-developer-specializations/infrastructure/devops-engineer/SKILL.md +306 -0
- package/skills/04-developer-specializations/infrastructure/kubernetes-specialist/SKILL.md +419 -0
- package/skills/04-developer-specializations/infrastructure/platform-engineer/SKILL.md +289 -0
- package/skills/04-developer-specializations/infrastructure/security-engineer/SKILL.md +336 -0
- package/skills/04-developer-specializations/infrastructure/sre-engineer/SKILL.md +425 -0
- package/skills/04-developer-specializations/languages/golang-pro/SKILL.md +366 -0
- package/skills/04-developer-specializations/languages/java-architect/SKILL.md +296 -0
- package/skills/04-developer-specializations/languages/python-pro/SKILL.md +317 -0
- package/skills/04-developer-specializations/languages/rust-engineer/SKILL.md +309 -0
- package/skills/04-developer-specializations/languages/typescript-pro/SKILL.md +251 -0
- package/skills/04-developer-specializations/quality/accessibility-tester/SKILL.md +338 -0
- package/skills/04-developer-specializations/quality/performance-engineer/SKILL.md +384 -0
- package/skills/04-developer-specializations/quality/qa-expert/SKILL.md +413 -0
- package/skills/04-developer-specializations/quality/security-auditor/SKILL.md +359 -0
- package/skills/05-specialists/compliance-specialist/SKILL.md +171 -0
- package/skills/using-locus/SKILL.md +124 -0
- package/.opencode/skills/locus/SKILL.md +0 -299
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Core Library
|
|
3
|
+
* Shared utilities for skill discovery, loading, and management
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
6
|
+
import { join, basename } from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Extract YAML frontmatter from a skill or agent file.
|
|
9
|
+
*
|
|
10
|
+
* Supports:
|
|
11
|
+
* - Simple key: value pairs
|
|
12
|
+
* - Multiline values with |
|
|
13
|
+
* - Nested objects (one level deep, e.g., metadata:)
|
|
14
|
+
*
|
|
15
|
+
* Format:
|
|
16
|
+
* ---
|
|
17
|
+
* name: skill-name
|
|
18
|
+
* description: Use when [condition] - [what it does]
|
|
19
|
+
* metadata:
|
|
20
|
+
* version: "1.0.0"
|
|
21
|
+
* tier: developer
|
|
22
|
+
* ---
|
|
23
|
+
*/
|
|
24
|
+
export function extractFrontmatter(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
27
|
+
// Normalize line endings (handle Windows \r\n)
|
|
28
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
29
|
+
let inFrontmatter = false;
|
|
30
|
+
const metadata = { name: '', description: '' };
|
|
31
|
+
let currentKey = '';
|
|
32
|
+
let multilineValue = '';
|
|
33
|
+
let inNestedObject = false;
|
|
34
|
+
let nestedKey = '';
|
|
35
|
+
const nestedObject = {};
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const trimmedLine = line.trim();
|
|
38
|
+
if (trimmedLine === '---') {
|
|
39
|
+
if (inFrontmatter)
|
|
40
|
+
break;
|
|
41
|
+
inFrontmatter = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFrontmatter) {
|
|
45
|
+
// Check for nested object entry (starts with 2 spaces)
|
|
46
|
+
if (inNestedObject && line.match(/^ {2}\w+:/)) {
|
|
47
|
+
const nestedMatch = line.match(/^ {2}(\w+):\s*(.*)$/);
|
|
48
|
+
if (nestedMatch) {
|
|
49
|
+
const [, key, value] = nestedMatch;
|
|
50
|
+
nestedObject[key] = value.trim().replace(/^["']|["']$/g, ''); // Strip quotes
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Check for top-level key
|
|
55
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
56
|
+
if (match) {
|
|
57
|
+
// Save previous nested object if any
|
|
58
|
+
if (inNestedObject && nestedKey) {
|
|
59
|
+
metadata[nestedKey] = { ...nestedObject };
|
|
60
|
+
// Also flatten common nested fields to top level for convenience
|
|
61
|
+
if (nestedKey === 'metadata') {
|
|
62
|
+
for (const [k, v] of Object.entries(nestedObject)) {
|
|
63
|
+
if (v && !metadata[k]) {
|
|
64
|
+
metadata[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
inNestedObject = false;
|
|
69
|
+
nestedKey = '';
|
|
70
|
+
// Clear nested object for next use
|
|
71
|
+
for (const k of Object.keys(nestedObject)) {
|
|
72
|
+
delete nestedObject[k];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Save previous multiline value if any
|
|
76
|
+
if (currentKey && multilineValue) {
|
|
77
|
+
metadata[currentKey] = multilineValue.trim();
|
|
78
|
+
}
|
|
79
|
+
const [, key, value] = match;
|
|
80
|
+
currentKey = key;
|
|
81
|
+
// Handle nested object (value is empty, next lines are indented)
|
|
82
|
+
if (value.trim() === '') {
|
|
83
|
+
inNestedObject = true;
|
|
84
|
+
nestedKey = key;
|
|
85
|
+
currentKey = '';
|
|
86
|
+
multilineValue = '';
|
|
87
|
+
}
|
|
88
|
+
// Handle multiline values starting with |
|
|
89
|
+
else if (value.trim() === '|') {
|
|
90
|
+
multilineValue = '';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
metadata[key] = value.trim();
|
|
94
|
+
currentKey = '';
|
|
95
|
+
multilineValue = '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (currentKey && line.startsWith(' ')) {
|
|
99
|
+
// Continuation of multiline value
|
|
100
|
+
multilineValue += line.trim() + ' ';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Save final nested object
|
|
105
|
+
if (inNestedObject && nestedKey) {
|
|
106
|
+
metadata[nestedKey] = { ...nestedObject };
|
|
107
|
+
if (nestedKey === 'metadata') {
|
|
108
|
+
for (const [k, v] of Object.entries(nestedObject)) {
|
|
109
|
+
if (v && !metadata[k]) {
|
|
110
|
+
metadata[k] = v;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Save final multiline value
|
|
116
|
+
if (currentKey && multilineValue) {
|
|
117
|
+
metadata[currentKey] = multilineValue.trim();
|
|
118
|
+
}
|
|
119
|
+
return metadata;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { name: '', description: '' };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Strip YAML frontmatter from content, returning just the body.
|
|
127
|
+
*/
|
|
128
|
+
export function stripFrontmatter(content) {
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
let inFrontmatter = false;
|
|
131
|
+
let frontmatterEnded = false;
|
|
132
|
+
const contentLines = [];
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (line.trim() === '---') {
|
|
135
|
+
if (inFrontmatter) {
|
|
136
|
+
frontmatterEnded = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
inFrontmatter = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (frontmatterEnded || !inFrontmatter) {
|
|
143
|
+
contentLines.push(line);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return contentLines.join('\n').trim();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Find all SKILL.md files in a directory recursively.
|
|
150
|
+
*/
|
|
151
|
+
export function findSkillsInDir(dir, sourceType, maxDepth = 4) {
|
|
152
|
+
const skills = [];
|
|
153
|
+
if (!existsSync(dir))
|
|
154
|
+
return skills;
|
|
155
|
+
function recurse(currentDir, depth) {
|
|
156
|
+
if (depth > maxDepth)
|
|
157
|
+
return;
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (entry.name.startsWith('.'))
|
|
167
|
+
continue;
|
|
168
|
+
const fullPath = join(currentDir, entry.name);
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
// Check for SKILL.md in this directory
|
|
171
|
+
const skillFile = join(fullPath, 'SKILL.md');
|
|
172
|
+
if (existsSync(skillFile)) {
|
|
173
|
+
const meta = extractFrontmatter(skillFile);
|
|
174
|
+
skills.push({
|
|
175
|
+
path: fullPath,
|
|
176
|
+
skillFile,
|
|
177
|
+
name: meta.name || entry.name,
|
|
178
|
+
description: meta.description || '',
|
|
179
|
+
sourceType,
|
|
180
|
+
tier: typeof meta.tier === 'string' ? meta.tier : undefined,
|
|
181
|
+
category: typeof meta.category === 'string' ? meta.category : undefined,
|
|
182
|
+
council: typeof meta.council === 'string' ? meta.council : undefined,
|
|
183
|
+
version: typeof meta.version === 'string' ? meta.version : undefined,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Recurse into subdirectories
|
|
187
|
+
recurse(fullPath, depth + 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
recurse(dir, 0);
|
|
192
|
+
return skills;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Find all agent definition files in a directory.
|
|
196
|
+
*/
|
|
197
|
+
export function findAgentsInDir(dir, maxDepth = 3) {
|
|
198
|
+
const agents = [];
|
|
199
|
+
if (!existsSync(dir))
|
|
200
|
+
return agents;
|
|
201
|
+
function recurse(currentDir, depth, category) {
|
|
202
|
+
if (depth > maxDepth)
|
|
203
|
+
return;
|
|
204
|
+
let entries;
|
|
205
|
+
try {
|
|
206
|
+
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.name.startsWith('.'))
|
|
213
|
+
continue;
|
|
214
|
+
const fullPath = join(currentDir, entry.name);
|
|
215
|
+
if (entry.isDirectory()) {
|
|
216
|
+
// Use directory name as category
|
|
217
|
+
recurse(fullPath, depth + 1, entry.name);
|
|
218
|
+
}
|
|
219
|
+
else if (entry.name.endsWith('.md')) {
|
|
220
|
+
const { name, description } = extractFrontmatter(fullPath);
|
|
221
|
+
const agentName = name || basename(entry.name, '.md');
|
|
222
|
+
agents.push({
|
|
223
|
+
path: fullPath,
|
|
224
|
+
name: agentName,
|
|
225
|
+
description: description || '',
|
|
226
|
+
category: category || 'general',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
recurse(dir, 0, '');
|
|
232
|
+
return agents;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolve a skill name to its file path, handling namespacing and shadowing.
|
|
236
|
+
* Priority: project > personal > locus
|
|
237
|
+
*
|
|
238
|
+
* Skill names can be:
|
|
239
|
+
* - "skill-name" - searches all locations
|
|
240
|
+
* - "project:skill-name" - forces project skills only
|
|
241
|
+
* - "locus:skill-name" - forces locus skills only
|
|
242
|
+
*/
|
|
243
|
+
export function resolveSkillPath(skillName, locusSkillsDir, personalSkillsDir, projectSkillsDir) {
|
|
244
|
+
// Parse namespace prefix
|
|
245
|
+
const forceProject = skillName.startsWith('project:');
|
|
246
|
+
const forceLocus = skillName.startsWith('locus:');
|
|
247
|
+
const actualSkillName = skillName
|
|
248
|
+
.replace(/^project:/, '')
|
|
249
|
+
.replace(/^locus:/, '');
|
|
250
|
+
// Try project skills first (unless forcing locus)
|
|
251
|
+
if (!forceLocus && projectSkillsDir) {
|
|
252
|
+
const resolved = findSkillByName(projectSkillsDir, actualSkillName, 'project');
|
|
253
|
+
if (resolved)
|
|
254
|
+
return resolved;
|
|
255
|
+
}
|
|
256
|
+
// If forcing project and not found, return null
|
|
257
|
+
if (forceProject)
|
|
258
|
+
return null;
|
|
259
|
+
// Try personal skills (unless forcing locus)
|
|
260
|
+
if (!forceLocus && personalSkillsDir) {
|
|
261
|
+
const resolved = findSkillByName(personalSkillsDir, actualSkillName, 'personal');
|
|
262
|
+
if (resolved)
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
265
|
+
// Try locus skills
|
|
266
|
+
const resolved = findSkillByName(locusSkillsDir, actualSkillName, 'locus');
|
|
267
|
+
if (resolved)
|
|
268
|
+
return resolved;
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Find a skill by name in a directory (searches recursively).
|
|
273
|
+
*/
|
|
274
|
+
function findSkillByName(baseDir, skillName, sourceType) {
|
|
275
|
+
if (!existsSync(baseDir))
|
|
276
|
+
return null;
|
|
277
|
+
// Direct path check
|
|
278
|
+
const directPath = join(baseDir, skillName);
|
|
279
|
+
const directSkillFile = join(directPath, 'SKILL.md');
|
|
280
|
+
if (existsSync(directSkillFile)) {
|
|
281
|
+
return {
|
|
282
|
+
skillFile: directSkillFile,
|
|
283
|
+
sourceType,
|
|
284
|
+
skillPath: skillName,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// Search in subdirectories (for categorized skills like 01-executive-suite/ceo-strategist)
|
|
288
|
+
const skills = findSkillsInDir(baseDir, sourceType);
|
|
289
|
+
for (const skill of skills) {
|
|
290
|
+
if (skill.name === skillName || basename(skill.path) === skillName) {
|
|
291
|
+
return {
|
|
292
|
+
skillFile: skill.skillFile,
|
|
293
|
+
sourceType,
|
|
294
|
+
skillPath: skill.path.replace(baseDir + '/', '').replace(baseDir + '\\', ''),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get the using-locus bootstrap skill content.
|
|
302
|
+
*/
|
|
303
|
+
export function getBootstrapContent(locusSkillsDir, compact = false) {
|
|
304
|
+
// Try to find the main locus skill
|
|
305
|
+
const usingLocusPath = join(locusSkillsDir, 'using-locus', 'SKILL.md');
|
|
306
|
+
const legacyLocusPath = join(locusSkillsDir, 'locus', 'SKILL.md');
|
|
307
|
+
let skillFile = null;
|
|
308
|
+
if (existsSync(usingLocusPath)) {
|
|
309
|
+
skillFile = usingLocusPath;
|
|
310
|
+
}
|
|
311
|
+
else if (existsSync(legacyLocusPath)) {
|
|
312
|
+
skillFile = legacyLocusPath;
|
|
313
|
+
}
|
|
314
|
+
if (!skillFile)
|
|
315
|
+
return null;
|
|
316
|
+
const fullContent = readFileSync(skillFile, 'utf-8');
|
|
317
|
+
const content = stripFrontmatter(fullContent);
|
|
318
|
+
const toolMapping = compact
|
|
319
|
+
? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
|
|
320
|
+
|
|
321
|
+
**Skills naming (priority order):** project: > personal: > locus:`
|
|
322
|
+
: `**Tool Mapping for OpenCode:**
|
|
323
|
+
When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
324
|
+
- \`TodoWrite\` -> \`update_plan\`
|
|
325
|
+
- \`Task\` tool with subagents -> Use OpenCode's subagent system (@mention)
|
|
326
|
+
- \`Skill\` tool -> \`use_skill\` custom tool
|
|
327
|
+
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` -> Your native tools
|
|
328
|
+
|
|
329
|
+
**Skills naming (priority order):**
|
|
330
|
+
- Project skills: \`project:skill-name\` (in .opencode/skills/ or project skills dir)
|
|
331
|
+
- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
|
|
332
|
+
- Locus skills: \`locus:skill-name\`
|
|
333
|
+
- Project skills override personal, which override locus when names match`;
|
|
334
|
+
return `<EXTREMELY_IMPORTANT>
|
|
335
|
+
You have access to Locus skills.
|
|
336
|
+
|
|
337
|
+
**IMPORTANT: The using-locus skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the use_skill tool to load "using-locus" - that would be redundant. Use use_skill only for OTHER skills.**
|
|
338
|
+
|
|
339
|
+
${content}
|
|
340
|
+
|
|
341
|
+
${toolMapping}
|
|
342
|
+
</EXTREMELY_IMPORTANT>`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Normalize a path: trim whitespace, expand ~, resolve to absolute.
|
|
346
|
+
*/
|
|
347
|
+
export function normalizePath(p, homeDir) {
|
|
348
|
+
if (!p || typeof p !== 'string')
|
|
349
|
+
return null;
|
|
350
|
+
let normalized = p.trim();
|
|
351
|
+
if (!normalized)
|
|
352
|
+
return null;
|
|
353
|
+
// Expand ~ to home directory
|
|
354
|
+
if (normalized.startsWith('~/')) {
|
|
355
|
+
normalized = join(homeDir, normalized.slice(2));
|
|
356
|
+
}
|
|
357
|
+
else if (normalized === '~') {
|
|
358
|
+
normalized = homeDir;
|
|
359
|
+
}
|
|
360
|
+
return normalized;
|
|
361
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Windows wrapper for running bash hooks
|
|
3
|
+
REM Tries: Git Bash > WSL > native cmd fallback
|
|
4
|
+
|
|
5
|
+
setlocal enabledelayedexpansion
|
|
6
|
+
|
|
7
|
+
set "HOOK_NAME=%~1"
|
|
8
|
+
set "SCRIPT_DIR=%~dp0"
|
|
9
|
+
set "HOOK_PATH=%SCRIPT_DIR%%HOOK_NAME%"
|
|
10
|
+
|
|
11
|
+
REM Try Git Bash first (most common on Windows dev machines)
|
|
12
|
+
where bash >nul 2>&1
|
|
13
|
+
if %ERRORLEVEL% EQU 0 (
|
|
14
|
+
bash "%HOOK_PATH%"
|
|
15
|
+
exit /b %ERRORLEVEL%
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
REM Try WSL
|
|
19
|
+
where wsl >nul 2>&1
|
|
20
|
+
if %ERRORLEVEL% EQU 0 (
|
|
21
|
+
wsl bash "%HOOK_PATH%"
|
|
22
|
+
exit /b %ERRORLEVEL%
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
REM Fallback to native Windows implementation
|
|
26
|
+
if "%HOOK_NAME%"=="session-start.sh" (
|
|
27
|
+
call "%SCRIPT_DIR%session-start.cmd"
|
|
28
|
+
exit /b %ERRORLEVEL%
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
REM Unknown hook, output minimal JSON
|
|
32
|
+
echo {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Locus skills available. Read skills/using-locus/SKILL.md to get started."}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Locus Bootstrap Hook for Claude Code (Windows native fallback)
|
|
3
|
+
REM Outputs JSON context for session injection
|
|
4
|
+
|
|
5
|
+
setlocal enabledelayedexpansion
|
|
6
|
+
|
|
7
|
+
set "SCRIPT_DIR=%~dp0"
|
|
8
|
+
set "PLUGIN_ROOT=%SCRIPT_DIR%.."
|
|
9
|
+
set "SKILL_FILE=%PLUGIN_ROOT%\skills\using-locus\SKILL.md"
|
|
10
|
+
|
|
11
|
+
REM Output the bootstrap JSON
|
|
12
|
+
REM Note: This is a simplified version since Windows cmd has limited string handling
|
|
13
|
+
echo {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"<EXTREMELY_IMPORTANT>\nYou have access to Locus skills for AI-powered project planning.\n\n**How to Use Skills:**\nRead the SKILL.md file to load a skill. Start with:\n- Read skills/using-locus/SKILL.md - Main planning workflow (Vision -> Features -> Design -> Build)\n\n**Available Skill Categories:**\n- skills/01-executive-suite/ - CEO, CTO, CPO, CFO, COO perspectives\n- skills/02-product-management/ - Product manager, project manager, scrum master\n- skills/03-engineering-leadership/ - Tech lead, staff engineer, principal engineer\n- skills/04-developer-specializations/ - Frontend, backend, devops, data, AI specialists\n- skills/05-specialists/ - Compliance and other specialists\n\n**The Rule:** If there's even a 1%% chance a skill might apply, load it and check.\n\n**Quick Start:** Say 'I want to build...' or use /locus command.\n</EXTREMELY_IMPORTANT>"}}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Locus Bootstrap Hook for Claude Code
|
|
3
|
+
# Outputs JSON context for session injection
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Get the directory where this script lives
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
10
|
+
|
|
11
|
+
# Read the using-locus skill content
|
|
12
|
+
SKILL_FILE="$PLUGIN_ROOT/skills/using-locus/SKILL.md"
|
|
13
|
+
|
|
14
|
+
if [ ! -f "$SKILL_FILE" ]; then
|
|
15
|
+
# Fallback: output minimal context
|
|
16
|
+
cat <<'EOF'
|
|
17
|
+
{
|
|
18
|
+
"hookSpecificOutput": {
|
|
19
|
+
"hookEventName": "SessionStart",
|
|
20
|
+
"additionalContext": "<EXTREMELY_IMPORTANT>\nYou have access to Locus skills for project planning.\n\nTo use a skill, read its SKILL.md file. Start with:\n- skills/using-locus/SKILL.md - Main planning workflow\n\nSkills are in: skills/01-executive-suite/, skills/02-product-management/, etc.\n</EXTREMELY_IMPORTANT>"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
EOF
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Read and escape skill content for JSON
|
|
28
|
+
SKILL_CONTENT=$(cat "$SKILL_FILE" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/\t/\\t/g')
|
|
29
|
+
|
|
30
|
+
# Build the bootstrap context
|
|
31
|
+
BOOTSTRAP="<EXTREMELY_IMPORTANT>
|
|
32
|
+
You have access to Locus skills.
|
|
33
|
+
|
|
34
|
+
**IMPORTANT: The using-locus skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the Skill tool to load \\\"using-locus\\\" - that would be redundant. Use Skill tool only for OTHER skills.**
|
|
35
|
+
|
|
36
|
+
--- SKILL CONTENT START ---
|
|
37
|
+
$SKILL_CONTENT
|
|
38
|
+
--- SKILL CONTENT END ---
|
|
39
|
+
|
|
40
|
+
**Tool Mapping for Claude Code:**
|
|
41
|
+
When skills reference tools you don't have, substitute Claude Code equivalents:
|
|
42
|
+
- \\\`TodoWrite\\\` -> Use the built-in todo/task tracking
|
|
43
|
+
- \\\`Task\\\` tool with subagents -> Use Claude's task delegation
|
|
44
|
+
- \\\`Skill\\\` tool -> Read the skill file directly with Read tool
|
|
45
|
+
- \\\`Read\\\`, \\\`Write\\\`, \\\`Edit\\\`, \\\`Bash\\\` -> Your native tools
|
|
46
|
+
|
|
47
|
+
**Skills Location:**
|
|
48
|
+
Skills are in the plugin directory under skills/:
|
|
49
|
+
- skills/using-locus/ - Main planning skill
|
|
50
|
+
- skills/01-executive-suite/ - Executive perspectives
|
|
51
|
+
- skills/02-product-management/ - Product skills
|
|
52
|
+
- skills/03-engineering-leadership/ - Tech leadership
|
|
53
|
+
- skills/04-developer-specializations/ - Developer skills
|
|
54
|
+
- skills/05-specialists/ - Specialist skills
|
|
55
|
+
|
|
56
|
+
To load a skill: Read the SKILL.md file in that directory.
|
|
57
|
+
</EXTREMELY_IMPORTANT>"
|
|
58
|
+
|
|
59
|
+
# Escape the bootstrap for JSON
|
|
60
|
+
ESCAPED_BOOTSTRAP=$(echo "$BOOTSTRAP" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
|
|
61
|
+
|
|
62
|
+
# Output the JSON
|
|
63
|
+
cat <<EOF
|
|
64
|
+
{
|
|
65
|
+
"hookSpecificOutput": {
|
|
66
|
+
"hookEventName": "SessionStart",
|
|
67
|
+
"additionalContext": "$ESCAPED_BOOTSTRAP"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
EOF
|
package/opencode.json
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
"$schema": "https://opencode.ai/config.json",
|
|
3
3
|
"command": {
|
|
4
4
|
"locus": {
|
|
5
|
-
"template": "
|
|
5
|
+
"template": "Use the use_skill tool to load 'locus:using-locus' and help the user plan their project.\n\nIf they provided a project idea: $ARGUMENTS\nStart with Step 1: Vision.\n\nIf no idea provided, ask what they want to build.",
|
|
6
6
|
"description": "Start or resume a project (Vision → Features → Design → Build)"
|
|
7
7
|
},
|
|
8
|
-
"locus-
|
|
9
|
-
"template": "
|
|
10
|
-
"description": "
|
|
8
|
+
"locus-skills": {
|
|
9
|
+
"template": "Use the find_skills tool to list all available skills.\n\nIf the user specified a filter: $ARGUMENTS\nApply it as a category, tier, or search filter.\n\nShow the results in a clean, organized format.",
|
|
10
|
+
"description": "List available skills (optionally filter by category/tier/search)"
|
|
11
11
|
},
|
|
12
|
-
"locus-
|
|
13
|
-
"template": "
|
|
14
|
-
"description": "
|
|
12
|
+
"locus-skill": {
|
|
13
|
+
"template": "Use the use_skill tool to load the requested skill: $ARGUMENTS\n\nIf no skill name provided, use find_skills to show available options.",
|
|
14
|
+
"description": "Load a specific skill by name"
|
|
15
|
+
},
|
|
16
|
+
"locus-agents": {
|
|
17
|
+
"template": "Use the find_agents tool to list all available agent definitions.\n\nShow them grouped by category with descriptions.",
|
|
18
|
+
"description": "List available agents"
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locus-product-planning",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-powered product planning for OpenCode - Vision → Features → Design → Build",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
12
|
},
|
|
13
|
-
"./
|
|
13
|
+
"./skills": "./skills",
|
|
14
|
+
"./skills/*": "./skills/*"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
|
-
"
|
|
18
|
+
"skills",
|
|
19
|
+
"agents",
|
|
20
|
+
"hooks",
|
|
21
|
+
".claude-plugin",
|
|
18
22
|
"opencode.json"
|
|
19
23
|
],
|
|
20
24
|
"scripts": {
|
|
@@ -24,15 +28,20 @@
|
|
|
24
28
|
"test:watch": "vitest",
|
|
25
29
|
"test:coverage": "vitest run --coverage",
|
|
26
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"lint:fix": "eslint . --fix",
|
|
27
33
|
"cli": "npx tsx openspec/bin/cli.ts"
|
|
28
34
|
},
|
|
29
35
|
"keywords": [
|
|
30
36
|
"opencode",
|
|
31
37
|
"opencode-plugin",
|
|
38
|
+
"claude",
|
|
39
|
+
"claude-code",
|
|
32
40
|
"ai",
|
|
33
41
|
"planning",
|
|
34
42
|
"project-management",
|
|
35
|
-
"workflow"
|
|
43
|
+
"workflow",
|
|
44
|
+
"skills"
|
|
36
45
|
],
|
|
37
46
|
"author": "swiggityswerve",
|
|
38
47
|
"license": "MIT",
|
|
@@ -53,9 +62,14 @@
|
|
|
53
62
|
}
|
|
54
63
|
},
|
|
55
64
|
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^9.39.2",
|
|
56
66
|
"@opencode-ai/plugin": "^1.1.28",
|
|
57
67
|
"@types/node": "^20.10.0",
|
|
68
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
69
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
70
|
+
"eslint": "^9.39.2",
|
|
58
71
|
"glob": "^10.3.10",
|
|
72
|
+
"globals": "^17.0.0",
|
|
59
73
|
"ts-node": "^10.9.2",
|
|
60
74
|
"tsx": "^4.21.0",
|
|
61
75
|
"typescript": "^5.3.0",
|