@owlmeans/agent-skills 0.1.15 → 0.1.16
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/README.md +46 -18
- package/agent-meta/manifest.json +15 -99
- package/agent-meta/skills/agent-memory/SKILL.md +8 -7
- package/agent-meta/skills/getting-started/SKILL.md +2 -1
- package/agent-meta/skills/git/SKILL.md +2 -1
- package/agent-meta/skills/memory-promotion/SKILL.md +5 -4
- package/agent-meta/skills/memory-recompact/SKILL.md +3 -3
- package/agent-meta/skills/reuse-code/SKILL.md +4 -3
- package/agent-meta/skills/router-plugins/SKILL.md +2 -1
- package/agent-meta/skills/scaffolding/SKILL.md +15 -13
- package/agent-meta/skills/self-education/SKILL.md +10 -10
- package/agent-meta/skills/shadcn-versions/SKILL.md +3 -2
- package/agent-meta/skills/shadcn-web/SKILL.md +2 -1
- package/agent-meta/skills/skill-authoring/SKILL.md +44 -39
- package/build/apply.d.ts +2 -1
- package/build/apply.d.ts.map +1 -1
- package/build/apply.js +40 -4
- package/build/apply.js.map +1 -1
- package/build/args.d.ts +4 -2
- package/build/args.d.ts.map +1 -1
- package/build/args.js +6 -7
- package/build/args.js.map +1 -1
- package/build/discover.d.ts +8 -5
- package/build/discover.d.ts.map +1 -1
- package/build/discover.js +13 -11
- package/build/discover.js.map +1 -1
- package/build/llm/index.d.ts +13 -0
- package/build/llm/index.d.ts.map +1 -0
- package/build/llm/index.js +13 -0
- package/build/llm/index.js.map +1 -0
- package/build/llm/manifest.d.ts +30 -0
- package/build/llm/manifest.d.ts.map +1 -0
- package/build/llm/manifest.js +59 -0
- package/build/llm/manifest.js.map +1 -0
- package/build/llm/plugin.d.ts +17 -0
- package/build/llm/plugin.d.ts.map +1 -0
- package/build/llm/plugin.js +115 -0
- package/build/llm/plugin.js.map +1 -0
- package/build/llm/resolve.d.ts +4 -0
- package/build/llm/resolve.d.ts.map +1 -0
- package/build/llm/resolve.js +145 -0
- package/build/llm/resolve.js.map +1 -0
- package/build/llm/types.d.ts +59 -0
- package/build/llm/types.d.ts.map +1 -0
- package/build/llm/types.js +3 -0
- package/build/llm/types.js.map +1 -0
- package/build/plan.d.ts.map +1 -1
- package/build/plan.js +7 -6
- package/build/plan.js.map +1 -1
- package/build/run.d.ts.map +1 -1
- package/build/run.js +9 -12
- package/build/run.js.map +1 -1
- package/package.json +23 -1
- package/src/apply.ts +41 -4
- package/src/args.ts +12 -9
- package/src/discover.ts +13 -14
- package/src/llm/index.ts +13 -0
- package/src/llm/manifest.ts +75 -0
- package/src/llm/plugin.ts +133 -0
- package/src/llm/resolve.ts +180 -0
- package/src/llm/types.ts +71 -0
- package/src/plan.ts +8 -6
- package/src/run.ts +8 -11
- package/tests/installer.test.ts +61 -45
- package/tests/llm-plugin.test.ts +158 -0
- package/agent-meta/instructions/agent-memory.instructions.md +0 -143
- package/agent-meta/instructions/getting-started.instructions.md +0 -62
- package/agent-meta/instructions/git.instructions.md +0 -52
- package/agent-meta/instructions/memory-promotion.instructions.md +0 -111
- package/agent-meta/instructions/memory-recompact.instructions.md +0 -73
- package/agent-meta/instructions/reuse-code.instructions.md +0 -50
- package/agent-meta/instructions/router-plugins.instructions.md +0 -46
- package/agent-meta/instructions/scaffolding.instructions.md +0 -68
- package/agent-meta/instructions/self-education.instructions.md +0 -85
- package/agent-meta/instructions/shadcn-versions.instructions.md +0 -47
- package/agent-meta/instructions/shadcn-web.instructions.md +0 -95
- package/agent-meta/instructions/skill-authoring.instructions.md +0 -66
package/tests/installer.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
|
|
@@ -14,12 +14,16 @@ import { applyInstall } from '../src/apply.js'
|
|
|
14
14
|
|
|
15
15
|
const BANNER = AUTO_GENERATED_BANNER
|
|
16
16
|
|
|
17
|
+
/** Where a skill lands in the target project. */
|
|
18
|
+
const skillTarget = (root: string, name: string): string =>
|
|
19
|
+
join(root, '.agents', 'skills', name, 'SKILL.md')
|
|
20
|
+
|
|
17
21
|
const makeFixtureManifest = (pkgName: string, version: string, entries: Array<{
|
|
18
22
|
kind: 'skill' | 'instruction'
|
|
19
23
|
name: string
|
|
20
24
|
category?: 'package-specific' | 'multi-package'
|
|
21
|
-
}
|
|
22
|
-
schemaVersion
|
|
25
|
+
}>, schemaVersion = 2) => ({
|
|
26
|
+
schemaVersion,
|
|
23
27
|
package: pkgName,
|
|
24
28
|
version,
|
|
25
29
|
generatedAt: '2026-06-10T00:00:00Z',
|
|
@@ -32,7 +36,7 @@ const makeFixtureManifest = (pkgName: string, version: string, entries: Array<{
|
|
|
32
36
|
? `skills/${e.name}/SKILL.md`
|
|
33
37
|
: `instructions/${e.name}.instructions.md`,
|
|
34
38
|
canonicalPath: e.kind === 'skill'
|
|
35
|
-
? `.
|
|
39
|
+
? `.agents/skills/${e.name}/SKILL.md`
|
|
36
40
|
: `.github/instructions/${e.name}.instructions.md`,
|
|
37
41
|
})),
|
|
38
42
|
})
|
|
@@ -42,13 +46,14 @@ const writeFixturePackage = (
|
|
|
42
46
|
pkgName: string,
|
|
43
47
|
version: string,
|
|
44
48
|
entries: Array<{ kind: 'skill' | 'instruction'; name: string }>,
|
|
49
|
+
schemaVersion = 2,
|
|
45
50
|
): void => {
|
|
46
51
|
const pkgDir = join(nmDir, '@owlmeans', pkgName.replace('@owlmeans/', ''))
|
|
47
52
|
const agentMetaDir = join(pkgDir, 'agent-meta')
|
|
48
53
|
mkdirSync(join(agentMetaDir, 'skills'), { recursive: true })
|
|
49
54
|
mkdirSync(join(agentMetaDir, 'instructions'), { recursive: true })
|
|
50
55
|
|
|
51
|
-
const manifest = makeFixtureManifest(pkgName, version, entries)
|
|
56
|
+
const manifest = makeFixtureManifest(pkgName, version, entries, schemaVersion)
|
|
52
57
|
writeFileSync(join(agentMetaDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8')
|
|
53
58
|
|
|
54
59
|
for (const e of entries) {
|
|
@@ -90,18 +95,26 @@ describe('discover()', () => {
|
|
|
90
95
|
expect(entries).toEqual([])
|
|
91
96
|
})
|
|
92
97
|
|
|
93
|
-
it('discovers
|
|
98
|
+
it('discovers skills from node_modules', () => {
|
|
94
99
|
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
95
100
|
{ kind: 'skill', name: 'context' },
|
|
96
|
-
{ kind: 'instruction', name: 'context' },
|
|
97
101
|
])
|
|
98
102
|
const entries = discover(tmpDir, { extras: false })
|
|
99
|
-
expect(entries).toHaveLength(
|
|
100
|
-
expect(entries.
|
|
101
|
-
|
|
103
|
+
expect(entries).toHaveLength(1)
|
|
104
|
+
expect(entries[0].name).toBe('context')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('drops instruction entries published by pre-v2 packages', () => {
|
|
108
|
+
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
109
|
+
{ kind: 'skill', name: 'context' },
|
|
110
|
+
{ kind: 'instruction', name: 'context' },
|
|
111
|
+
], 1)
|
|
112
|
+
const entries = discover(tmpDir, { extras: false })
|
|
113
|
+
expect(entries).toHaveLength(1)
|
|
114
|
+
expect(entries.every(e => e.kind === 'skill')).toBe(true)
|
|
102
115
|
})
|
|
103
116
|
|
|
104
|
-
it('deduplicates by
|
|
117
|
+
it('deduplicates by name, keeping highest version', () => {
|
|
105
118
|
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.5', [
|
|
106
119
|
{ kind: 'skill', name: 'context' },
|
|
107
120
|
])
|
|
@@ -125,37 +138,17 @@ describe('discover()', () => {
|
|
|
125
138
|
expect(entries[0].name).toBe('context')
|
|
126
139
|
})
|
|
127
140
|
|
|
128
|
-
it('--claude-only filter', () => {
|
|
129
|
-
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
130
|
-
{ kind: 'skill', name: 'context' },
|
|
131
|
-
{ kind: 'instruction', name: 'context' },
|
|
132
|
-
])
|
|
133
|
-
const entries = discover(tmpDir, { extras: false, claudeOnly: true })
|
|
134
|
-
expect(entries.every(e => e.kind === 'skill')).toBe(true)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('--copilot-only filter', () => {
|
|
138
|
-
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
139
|
-
{ kind: 'skill', name: 'context' },
|
|
140
|
-
{ kind: 'instruction', name: 'context' },
|
|
141
|
-
])
|
|
142
|
-
const entries = discover(tmpDir, { extras: false, copilotOnly: true })
|
|
143
|
-
expect(entries.every(e => e.kind === 'instruction')).toBe(true)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
141
|
it('discovers entries nested in workspace package node_modules (bun monorepo)', () => {
|
|
147
142
|
// Scaffolded bun workspace: root has no @owlmeans; deps live under sources/*.
|
|
148
143
|
writeFixturePackage(join(tmpDir, 'sources', 'web', 'node_modules'), '@owlmeans/web-panel', '0.1.9', [
|
|
149
144
|
{ kind: 'skill', name: 'web-panel' },
|
|
150
|
-
{ kind: 'instruction', name: 'web-panel' },
|
|
151
145
|
])
|
|
152
146
|
writeFixturePackage(join(tmpDir, 'sources', 'api', 'node_modules'), '@owlmeans/server-app', '0.1.9', [
|
|
153
147
|
{ kind: 'skill', name: 'server-app' },
|
|
154
148
|
])
|
|
155
149
|
const entries = discover(tmpDir, { extras: false })
|
|
156
|
-
expect(entries.some(e => e.
|
|
157
|
-
expect(entries.some(e => e.
|
|
158
|
-
expect(entries.some(e => e.kind === 'skill' && e.name === 'server-app')).toBe(true)
|
|
150
|
+
expect(entries.some(e => e.name === 'web-panel')).toBe(true)
|
|
151
|
+
expect(entries.some(e => e.name === 'server-app')).toBe(true)
|
|
159
152
|
})
|
|
160
153
|
|
|
161
154
|
it('combines root and nested node_modules', () => {
|
|
@@ -231,7 +224,7 @@ describe('detectLinked()', () => {
|
|
|
231
224
|
// ---------------------------------------------------------------------------
|
|
232
225
|
|
|
233
226
|
describe('planInstall()', () => {
|
|
234
|
-
it('marks missing targets as install', () => {
|
|
227
|
+
it('targets .agents/skills and marks missing targets as install', () => {
|
|
235
228
|
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
236
229
|
{ kind: 'skill', name: 'context' },
|
|
237
230
|
])
|
|
@@ -239,6 +232,7 @@ describe('planInstall()', () => {
|
|
|
239
232
|
const items = planInstall(entries, tmpDir)
|
|
240
233
|
expect(items).toHaveLength(1)
|
|
241
234
|
expect(items[0].action).toBe('install')
|
|
235
|
+
expect(items[0].targetPath).toBe(skillTarget(tmpDir, 'context'))
|
|
242
236
|
})
|
|
243
237
|
|
|
244
238
|
it('marks identical files as skip-uptodate', () => {
|
|
@@ -247,7 +241,7 @@ describe('planInstall()', () => {
|
|
|
247
241
|
])
|
|
248
242
|
const entries = discover(tmpDir, { extras: false })
|
|
249
243
|
// Write same content to target
|
|
250
|
-
const targetDir = join(tmpDir, '.
|
|
244
|
+
const targetDir = join(tmpDir, '.agents', 'skills', 'context')
|
|
251
245
|
mkdirSync(targetDir, { recursive: true })
|
|
252
246
|
const source = readFileSync(entries[0].sourcePath, 'utf8')
|
|
253
247
|
writeFileSync(join(targetDir, 'SKILL.md'), source, 'utf8')
|
|
@@ -261,7 +255,7 @@ describe('planInstall()', () => {
|
|
|
261
255
|
{ kind: 'skill', name: 'context' },
|
|
262
256
|
])
|
|
263
257
|
const entries = discover(tmpDir, { extras: false })
|
|
264
|
-
const targetDir = join(tmpDir, '.
|
|
258
|
+
const targetDir = join(tmpDir, '.agents', 'skills', 'context')
|
|
265
259
|
mkdirSync(targetDir, { recursive: true })
|
|
266
260
|
// Write old managed content (has banner, but different)
|
|
267
261
|
writeFileSync(join(targetDir, 'SKILL.md'), `${BANNER}\n# Old content\n`, 'utf8')
|
|
@@ -275,7 +269,7 @@ describe('planInstall()', () => {
|
|
|
275
269
|
{ kind: 'skill', name: 'context' },
|
|
276
270
|
])
|
|
277
271
|
const entries = discover(tmpDir, { extras: false })
|
|
278
|
-
const targetDir = join(tmpDir, '.
|
|
272
|
+
const targetDir = join(tmpDir, '.agents', 'skills', 'context')
|
|
279
273
|
mkdirSync(targetDir, { recursive: true })
|
|
280
274
|
writeFileSync(join(targetDir, 'SKILL.md'), '# My custom skill\n', 'utf8')
|
|
281
275
|
|
|
@@ -288,7 +282,7 @@ describe('planInstall()', () => {
|
|
|
288
282
|
{ kind: 'skill', name: 'context' },
|
|
289
283
|
])
|
|
290
284
|
const entries = discover(tmpDir, { extras: false })
|
|
291
|
-
const targetDir = join(tmpDir, '.
|
|
285
|
+
const targetDir = join(tmpDir, '.agents', 'skills', 'context')
|
|
292
286
|
mkdirSync(targetDir, { recursive: true })
|
|
293
287
|
writeFileSync(join(targetDir, 'SKILL.md'), '# My custom skill\n', 'utf8')
|
|
294
288
|
|
|
@@ -298,21 +292,19 @@ describe('planInstall()', () => {
|
|
|
298
292
|
})
|
|
299
293
|
|
|
300
294
|
// ---------------------------------------------------------------------------
|
|
301
|
-
// applyInstall() — idempotency
|
|
295
|
+
// applyInstall() — idempotency and the Claude Code bridge
|
|
302
296
|
// ---------------------------------------------------------------------------
|
|
303
297
|
|
|
304
298
|
describe('applyInstall()', () => {
|
|
305
|
-
it('writes install items', () => {
|
|
299
|
+
it('writes install items into .agents/skills', () => {
|
|
306
300
|
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
307
301
|
{ kind: 'skill', name: 'context' },
|
|
308
|
-
{ kind: 'instruction', name: 'context' },
|
|
309
302
|
])
|
|
310
303
|
const entries = discover(tmpDir, { extras: false })
|
|
311
304
|
const items = planInstall(entries, tmpDir)
|
|
312
305
|
const result = applyInstall(items)
|
|
313
|
-
expect(result.installed).toBe(
|
|
314
|
-
expect(existsSync(
|
|
315
|
-
expect(existsSync(join(tmpDir, '.github', 'instructions', 'context.instructions.md'))).toBe(true)
|
|
306
|
+
expect(result.installed).toBe(1)
|
|
307
|
+
expect(existsSync(skillTarget(tmpDir, 'context'))).toBe(true)
|
|
316
308
|
})
|
|
317
309
|
|
|
318
310
|
it('is idempotent — second run yields all skip-uptodate', () => {
|
|
@@ -335,7 +327,7 @@ describe('applyInstall()', () => {
|
|
|
335
327
|
{ kind: 'skill', name: 'context' },
|
|
336
328
|
])
|
|
337
329
|
const entries = discover(tmpDir, { extras: false })
|
|
338
|
-
const targetDir = join(tmpDir, '.
|
|
330
|
+
const targetDir = join(tmpDir, '.agents', 'skills', 'context')
|
|
339
331
|
mkdirSync(targetDir, { recursive: true })
|
|
340
332
|
const localContent = '# My custom skill\n'
|
|
341
333
|
writeFileSync(join(targetDir, 'SKILL.md'), localContent, 'utf8')
|
|
@@ -345,4 +337,28 @@ describe('applyInstall()', () => {
|
|
|
345
337
|
// File should remain unchanged
|
|
346
338
|
expect(readFileSync(join(targetDir, 'SKILL.md'), 'utf8')).toBe(localContent)
|
|
347
339
|
})
|
|
340
|
+
|
|
341
|
+
it('symlinks installed skills into .claude/skills when the project uses Claude Code', () => {
|
|
342
|
+
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
343
|
+
{ kind: 'skill', name: 'context' },
|
|
344
|
+
])
|
|
345
|
+
mkdirSync(join(tmpDir, '.claude'), { recursive: true })
|
|
346
|
+
const entries = discover(tmpDir, { extras: false })
|
|
347
|
+
const result = applyInstall(planInstall(entries, tmpDir), tmpDir)
|
|
348
|
+
|
|
349
|
+
const link = join(tmpDir, '.claude', 'skills', 'context')
|
|
350
|
+
expect(result.linked).toBe(1)
|
|
351
|
+
expect(lstatSync(link).isSymbolicLink()).toBe(true)
|
|
352
|
+
expect(readlinkSync(link)).toBe(join('..', '..', '.agents', 'skills', 'context'))
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('leaves a project without .claude/ untouched', () => {
|
|
356
|
+
writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
|
|
357
|
+
{ kind: 'skill', name: 'context' },
|
|
358
|
+
])
|
|
359
|
+
const entries = discover(tmpDir, { extras: false })
|
|
360
|
+
const result = applyInstall(planInstall(entries, tmpDir), tmpDir)
|
|
361
|
+
expect(result.linked).toBe(0)
|
|
362
|
+
expect(existsSync(join(tmpDir, '.claude'))).toBe(false)
|
|
363
|
+
})
|
|
348
364
|
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { PromptBlock } from '@owlmeans/llm-common'
|
|
3
|
+
import type { LlmFileProvider } from '@owlmeans/llm-common'
|
|
4
|
+
import { anthropicPlugin, makePromptService } from '@owlmeans/llm'
|
|
5
|
+
import type { ModelConfig, PromptService } from '@owlmeans/llm'
|
|
6
|
+
import { owlmeansPackagesPlugin, stripMeta, unscoped } from '@owlmeans/agent-skills/llm'
|
|
7
|
+
|
|
8
|
+
const model = anthropicPlugin.build({
|
|
9
|
+
alias: 'spec',
|
|
10
|
+
secret: 'sk-test',
|
|
11
|
+
callbacks: [],
|
|
12
|
+
config: { alias: 'spec', model: 'claude-haiku-4-5-20251001' } as ModelConfig,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let seq = 0
|
|
16
|
+
const compose = (svc: PromptService, text: string) =>
|
|
17
|
+
svc.compose({}, [{ role: 'user', content: text }], { model, provider: anthropicPlugin })
|
|
18
|
+
|
|
19
|
+
const withPlugin = (options: Parameters<typeof owlmeansPackagesPlugin>[0] = {}): PromptService =>
|
|
20
|
+
makePromptService(
|
|
21
|
+
{ plugins: [owlmeansPackagesPlugin({ fetch: false, ...options })] },
|
|
22
|
+
`spec-pkg-${seq++}`,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/** A host file provider that serves one package, recording every path it is asked for. */
|
|
26
|
+
const fakeProvider = (asked: string[], body: string): LlmFileProvider => ({
|
|
27
|
+
getSourceList: async () => [],
|
|
28
|
+
writeFile: async () => { },
|
|
29
|
+
deleteFile: async () => { },
|
|
30
|
+
readFile: async path => {
|
|
31
|
+
asked.push(path)
|
|
32
|
+
if (path.endsWith('manifest.json')) {
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
package: '@owlmeans/auth',
|
|
36
|
+
version: '9.9.9',
|
|
37
|
+
generatedAt: '',
|
|
38
|
+
canonicalRepo: '',
|
|
39
|
+
entries: [{
|
|
40
|
+
kind: 'skill',
|
|
41
|
+
name: 'auth',
|
|
42
|
+
category: 'package-specific',
|
|
43
|
+
file: 'skills/auth/SKILL.md',
|
|
44
|
+
canonicalPath: '.claude/skills/auth/SKILL.md',
|
|
45
|
+
}],
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
return `---\nname: auth\n---\n${body}`
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('@owlmeans/agent-skills — embedded metadata', () => {
|
|
53
|
+
test('frontmatter and the generated banner are stripped from a skill body', () => {
|
|
54
|
+
const body = stripMeta([
|
|
55
|
+
'---',
|
|
56
|
+
'name: auth',
|
|
57
|
+
'description: something',
|
|
58
|
+
'---',
|
|
59
|
+
'<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->',
|
|
60
|
+
'',
|
|
61
|
+
'# @owlmeans/auth',
|
|
62
|
+
].join('\n'))
|
|
63
|
+
expect(body).toBe('# @owlmeans/auth')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('content without frontmatter survives untouched', () => {
|
|
67
|
+
expect(stripMeta('# Plain\n\nbody')).toBe('# Plain\n\nbody')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('the repo lays packages out under their unscoped name', () => {
|
|
71
|
+
expect(unscoped('@owlmeans/llm-common')).toBe('llm-common')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('@owlmeans/agent-skills — package skills plugin', () => {
|
|
76
|
+
test('a mentioned package contributes its skills to the packages block', async () => {
|
|
77
|
+
const result = await compose(withPlugin(), 'How do I use @owlmeans/auth in a handler?')
|
|
78
|
+
const packages = result.blocks.find(block => block.block === PromptBlock.Packages)
|
|
79
|
+
expect(packages?.text).toContain('@owlmeans/auth — auth')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('a package nobody mentioned is not loaded', async () => {
|
|
83
|
+
const result = await compose(withPlugin(), 'How do I write a handler?')
|
|
84
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('an excluded package is skipped even when mentioned', async () => {
|
|
88
|
+
const result = await compose(
|
|
89
|
+
withPlugin({ exclude: ['@owlmeans/auth'] }), 'How do I use @owlmeans/auth?'
|
|
90
|
+
)
|
|
91
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Two prompts that name the same packages in a different order must produce the same
|
|
95
|
+
// bytes, or they can never share a cache entry.
|
|
96
|
+
test('mention order does not change the composed bytes', async () => {
|
|
97
|
+
const first = await compose(withPlugin(), 'compare @owlmeans/auth with @owlmeans/context')
|
|
98
|
+
const second = await compose(withPlugin(), 'compare @owlmeans/context with @owlmeans/auth')
|
|
99
|
+
const packages = (result: typeof first) =>
|
|
100
|
+
result.blocks.find(block => block.block === PromptBlock.Packages)?.text
|
|
101
|
+
expect(packages(second)).toBe(packages(first))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('trailing punctuation is not part of the package name', async () => {
|
|
105
|
+
const result = await compose(withPlugin(), 'What about @owlmeans/auth. And @owlmeans/auth,')
|
|
106
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
|
|
107
|
+
.toContain('@owlmeans/auth — auth')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a provider wired at construction is preferred over the local filesystem', async () => {
|
|
111
|
+
const asked: string[] = []
|
|
112
|
+
const files = fakeProvider(asked, 'HOSTED BODY')
|
|
113
|
+
const result = await compose(withPlugin({ files: () => files }), 'about @owlmeans/auth')
|
|
114
|
+
expect(asked[0]).toBe('node_modules/@owlmeans/auth/agent-meta/manifest.json')
|
|
115
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
|
|
116
|
+
.toContain('HOSTED BODY')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// The host's provider is per-execution (a sandbox, a slot, one checkout), so it can only
|
|
120
|
+
// arrive on the compose context — a plugin built once at context composition cannot know
|
|
121
|
+
// it. Reading only the constructor option left this path dead.
|
|
122
|
+
test('a provider supplied on the compose context is used and preferred', async () => {
|
|
123
|
+
const asked: string[] = []
|
|
124
|
+
const files = fakeProvider(asked, 'CONTEXT BODY')
|
|
125
|
+
const svc = withPlugin()
|
|
126
|
+
const result = await svc.compose(
|
|
127
|
+
{},
|
|
128
|
+
[{ role: 'user', content: 'about @owlmeans/auth' }],
|
|
129
|
+
{ model, provider: anthropicPlugin, files: () => files },
|
|
130
|
+
)
|
|
131
|
+
expect(asked[0]).toBe('node_modules/@owlmeans/auth/agent-meta/manifest.json')
|
|
132
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
|
|
133
|
+
.toContain('CONTEXT BODY')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('two providers do not share each other\'s cached packages', async () => {
|
|
137
|
+
const svc = withPlugin()
|
|
138
|
+
const one = fakeProvider([], 'FIRST PROJECT')
|
|
139
|
+
const two = fakeProvider([], 'SECOND PROJECT')
|
|
140
|
+
const run = (files: LlmFileProvider) => svc.compose(
|
|
141
|
+
{},
|
|
142
|
+
[{ role: 'user', content: 'about @owlmeans/auth' }],
|
|
143
|
+
{ model, provider: anthropicPlugin, files: () => files },
|
|
144
|
+
)
|
|
145
|
+
const first = await run(one)
|
|
146
|
+
const second = await run(two)
|
|
147
|
+
const packages = (r: typeof first) =>
|
|
148
|
+
r.blocks.find(block => block.block === PromptBlock.Packages)?.text
|
|
149
|
+
expect(packages(first)).toContain('FIRST PROJECT')
|
|
150
|
+
expect(packages(second)).toContain('SECOND PROJECT')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// A prompt plugin that throws takes the whole model call with it.
|
|
154
|
+
test('an unreachable package degrades the prompt instead of failing the call', async () => {
|
|
155
|
+
const result = await compose(withPlugin(), 'about @owlmeans/does-not-exist-anywhere')
|
|
156
|
+
expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Shared agent memory protocol for the .agents/memory/ graph store (single store for Copilot and Claude Code) — MEMORY.md index, subsystem nodes, compact-on-write, size caps, read protocol. Apply when reading or writing project memory, or when any legacy .claude/memory or .github/memory path appears."
|
|
3
|
-
applyTo: "**/.agents/memory/**, **/.claude/memory/**, **/.github/memory/**"
|
|
4
|
-
scope: general
|
|
5
|
-
---
|
|
6
|
-
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
-
|
|
8
|
-
# Agent memory (`.agents/memory/`)
|
|
9
|
-
|
|
10
|
-
One shared memory store per repository, used by **both** GitHub Copilot and Claude Code: a graph
|
|
11
|
-
of subsystem nodes rooted in the `MEMORY.md` index. Compactness is the core value — every write
|
|
12
|
-
merges and compacts; nothing is ever appended as a log.
|
|
13
|
-
|
|
14
|
-
Never write memory to `.claude/memory/`, `.github/memory/`, `~/.copilot/`, `~/.claude/`, or
|
|
15
|
-
anywhere outside the repository. Legacy `.claude/memory/` / `.github/memory/` stores are
|
|
16
|
-
retired — if one exists, flag it for migration
|
|
17
|
-
(`.github/instructions/memory-recompact.instructions.md`) instead of writing there.
|
|
18
|
-
|
|
19
|
-
## Store layout
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
.agents/memory/
|
|
23
|
-
├── MEMORY.md # graph root: the only always-read file
|
|
24
|
-
└── <node>.md # one file per subsystem / concern / integration
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Flat directory — graph structure lives in scopes and wiki-links, not subdirectories.
|
|
28
|
-
|
|
29
|
-
## Node naming
|
|
30
|
-
|
|
31
|
-
Node id = lowercase-kebab name of the **project-structure part** the knowledge attaches to:
|
|
32
|
-
|
|
33
|
-
1. **Subsystem** — workspace dir / package name: `manager-web`, `auth`, `vslots`. Deepest
|
|
34
|
-
unambiguous segment; prefix with the parent only on collision.
|
|
35
|
-
2. **Cross-cutting concern** spanning subsystems: `build`, `testing`, `deploy`, `routing`, `i18n`.
|
|
36
|
-
3. **Integration** — external service/library: `stripe`, `cloudflare`, `openrouter`.
|
|
37
|
-
4. **`workspace`** — repo-level facts that fit nowhere else (remotes, branch topology, env model).
|
|
38
|
-
|
|
39
|
-
File is `<node>.md`; the id is the wiki-link target: `[[manager-web]]`. **Never** key a node to an
|
|
40
|
-
event, date, phase, investigation, or task name.
|
|
41
|
-
|
|
42
|
-
## Index format (`MEMORY.md`)
|
|
43
|
-
|
|
44
|
-
```markdown
|
|
45
|
-
# Memory Graph — <project>
|
|
46
|
-
|
|
47
|
-
Shared agent memory (`agent-memory` protocol). Read this file at session start.
|
|
48
|
-
Before non-trivial work, open every node whose scope matches the task's files or topics.
|
|
49
|
-
|
|
50
|
-
## Subsystems
|
|
51
|
-
- [[manager-web]] `sources/manager-web/**` — i18n 8-domain split; owl-theme wiring
|
|
52
|
-
## Cross-cutting
|
|
53
|
-
- [[build]] `**/tsconfig*.json` — tsbuildinfo path pinning; tolerant library builds
|
|
54
|
-
## Integrations
|
|
55
|
-
- [[stripe]] `ext:stripe` — sandbox topology; webhook fan-out
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Rules: exactly one line per node — `- [[<node>]] ` + backticked scope + ` — <hook>`; hook ≤ 100
|
|
59
|
-
chars; no dates; only non-empty groups; file ≤ 50 lines; no skills listed (skills self-describe).
|
|
60
|
-
The scope column is the router: match the task's paths/topics against scopes to decide which
|
|
61
|
-
nodes to open.
|
|
62
|
-
|
|
63
|
-
## Node format
|
|
64
|
-
|
|
65
|
-
Frontmatter — exactly three fields:
|
|
66
|
-
|
|
67
|
-
```yaml
|
|
68
|
-
---
|
|
69
|
-
node: vslots # = filename without .md; = wiki-link target
|
|
70
|
-
scope: "scripts/vslots/**" # repo-relative globs; ext:<service> for integrations; . for workspace
|
|
71
|
-
updated: 2026-08 # month granularity only
|
|
72
|
-
---
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Body — only non-empty sections, in this order:
|
|
76
|
-
|
|
77
|
-
- `## Facts` — what IS: structure, behavior, topology. 1–3 lines each.
|
|
78
|
-
- `## Invariants` — what MUST HOLD; optionally "broke when violated: <symptom>".
|
|
79
|
-
- `## Gotchas` — symptom → cause → counter-move, ≤ 3 lines each.
|
|
80
|
-
- `## Pointers` — key files, promoted-skill pointer lines, `External docs` (URL + gist).
|
|
81
|
-
- `## Status` — optional; in-flight state only; every line dated; prune resolved lines on every read.
|
|
82
|
-
|
|
83
|
-
`Status` is the **only** place dates and unfinished-work notes are allowed. Link related nodes
|
|
84
|
-
inline with `[[node]]`.
|
|
85
|
-
|
|
86
|
-
## Read protocol
|
|
87
|
-
|
|
88
|
-
- **Session start**: read `.agents/memory/MEMORY.md` — the index only.
|
|
89
|
-
- **Before a non-trivial task**: open every node whose scope matches the task (typically 1–3).
|
|
90
|
-
- **When a topic surfaces mid-session** that the index names: open its node before acting.
|
|
91
|
-
- **Never bulk-read** the store — the index + scopes exist so you don't have to.
|
|
92
|
-
|
|
93
|
-
## Write protocol (compact-on-write)
|
|
94
|
-
|
|
95
|
-
Every write is a merge, never an append:
|
|
96
|
-
|
|
97
|
-
1. **Extract** — reduce the outcome to reusable knowledge atoms (rule below). Nothing survives →
|
|
98
|
-
write nothing.
|
|
99
|
-
2. **Locate** — match scopes to find the node; none matches → create one per the naming rule and
|
|
100
|
-
add its index line.
|
|
101
|
-
3. **Merge / supersede** — place each atom in the right section; if it refines or contradicts an
|
|
102
|
-
existing line, **rewrite that line in place** — never append a dated correction. The node
|
|
103
|
-
always reads as current truth.
|
|
104
|
-
4. **Compact** — reread the node; collapse redundancy; delete anything the code or git history
|
|
105
|
-
now states; prune resolved `Status` lines.
|
|
106
|
-
5. **Cap** — over soft cap: compact harder. Over hard cap: split by sub-scope into a linked child
|
|
107
|
-
node, or promote procedure-shaped overflow
|
|
108
|
-
(`.github/instructions/memory-promotion.instructions.md`, else the skill-authoring guidance).
|
|
109
|
-
6. **Index** — update the hook line if the node's center of gravity moved; bump `updated:`.
|
|
110
|
-
|
|
111
|
-
## Knowledge, not events
|
|
112
|
-
|
|
113
|
-
**Record the rule, not the story.** For every line ask: *what must a future agent know to act
|
|
114
|
-
correctly, stated without reference to this session?* Keep invariants, cause→effect,
|
|
115
|
-
counter-moves, recognition fingerprints (symptoms). Drop dates, phase numbers, who did what,
|
|
116
|
-
attempt sequences, and anything recoverable from code or git.
|
|
117
|
-
|
|
118
|
-
Before (event log): "Verification 2026-06-11 … Bug found: script missing embedded-count logic.
|
|
119
|
-
Fix deployed to all copies; second run idempotent."
|
|
120
|
-
After (knowledge): "`nested-agent-context.sh` exists as byte-identical copies in the archive and
|
|
121
|
-
each repo — fan every fix out to all copies; verify with `diff`. Correct re-runs are no-ops."
|
|
122
|
-
|
|
123
|
-
## Size caps (hard)
|
|
124
|
-
|
|
125
|
-
| Thing | Cap |
|
|
126
|
-
|---|---|
|
|
127
|
-
| Index entry | 1 line, hook ≤ 100 chars |
|
|
128
|
-
| `MEMORY.md` | ≤ 50 lines |
|
|
129
|
-
| Node file (soft / hard) | 80 / 120 lines |
|
|
130
|
-
| Single fact/gotcha | ≤ 3 lines |
|
|
131
|
-
| `Status` section | ≤ 5 dated lines |
|
|
132
|
-
| Nodes per store (soft) | ~25 — merge low-traffic siblings beyond |
|
|
133
|
-
|
|
134
|
-
## Memory vs instruction
|
|
135
|
-
|
|
136
|
-
Fact-shaped ("what is true") stays here. Procedure-shaped ("to do X, do Y") becomes an
|
|
137
|
-
instruction/skill — follow `.github/instructions/memory-promotion.instructions.md` where present,
|
|
138
|
-
otherwise the skill-authoring guidance. Repeated use of a node to *perform* tasks, and over-cap
|
|
139
|
-
nodes full of steps, are promotion triggers.
|
|
140
|
-
|
|
141
|
-
Promotion is a **rewrite, never a move**: never paste node text into an instruction. "Record the
|
|
142
|
-
rule, not the story" and the size caps above bind instruction bodies at least as tightly as they
|
|
143
|
-
bind nodes (memory-promotion instruction → Distillation).
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "How to build a fullstack OwlMeans Common app — the common/api/web three-workspace pattern, server and web context bootstrap, shared entrypoints + elevate(), and a session-scoped in-memory resource via @owlmeans/static-resource. Apply when starting a new OwlMeans project or wiring web↔api."
|
|
3
|
-
applyTo: "**/modules.ts, **/context.ts, **/config.ts, **/render.tsx, **/index.tsx, **/sources/**"
|
|
4
|
-
scope: general
|
|
5
|
-
---
|
|
6
|
-
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
-
|
|
8
|
-
# Building a fullstack OwlMeans Common app
|
|
9
|
-
|
|
10
|
-
A minimal app is a **bun-workspace monorepo with three packages**:
|
|
11
|
-
|
|
12
|
-
- `sources/common` — shared **entrypoints** (routes), AJV schemas, types and config. The single
|
|
13
|
-
source of truth.
|
|
14
|
-
- `sources/api` — `@owlmeans/server-app` backend; handlers attached to the shared entrypoints.
|
|
15
|
-
- `sources/web` — `@owlmeans/web-panel` + shadcn UI; screens attached to the same entrypoints.
|
|
16
|
-
|
|
17
|
-
Full walkthrough: `docs/getting-started.md`. To generate it: `npm create @owlmeans/app`.
|
|
18
|
-
|
|
19
|
-
## One contract, two sides
|
|
20
|
-
|
|
21
|
-
Declare each route once in `common`, then `elevate()` it on both sides:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
// common/modules.ts
|
|
25
|
-
export const sessionModules = [
|
|
26
|
-
entrypoint(route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
|
|
27
|
-
filter(params(SessionParamsSchema))),
|
|
28
|
-
]
|
|
29
|
-
// api/modules.ts → elevate(sessionModules, session.list, handlers.list)
|
|
30
|
-
// web/modules.ts → elevate(modules, session.list) // call-only; screens via handler(Component)
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Config
|
|
34
|
-
|
|
35
|
-
`common/config.ts` registers both services; `base: 'api'` prefixes API routes with `/api`:
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
const cfg = service({ type: AppType.Frontend, service: APP_WEB, host: 'localhost', port: 3001 })
|
|
39
|
-
service({ type: AppType.Backend, service: APP_API, host: 'localhost', port: 3000, base: 'api' }, cfg)
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Backend + in-memory data
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
const context = makeContext(cfg, true)
|
|
46
|
-
appendStaticResource(context, SESSION_ITEMS) // @owlmeans/static-resource — no DB
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Handlers (`handleRequest`/`handleBody`/`handleParams`) use `ctx.getStaticResource<T>(alias)`.
|
|
50
|
-
`static-resource.list()` accepts no criteria — list all and filter in JS. `main(context, appModules)`
|
|
51
|
-
starts the server. Swap for `@owlmeans/mongo-resource` / `redis-resource` when you need persistence.
|
|
52
|
-
|
|
53
|
-
## Web (shadcn)
|
|
54
|
-
|
|
55
|
-
`web-panel`'s `PanelApp` is shadcn/Tailwind v4. The app **provides** `@/lib/utils` and
|
|
56
|
-
`@/components/ui/{alert,button,card,input,label,progress}` at the `@` alias (→ `src`). Render with
|
|
57
|
-
`provide` from `@owlmeans/web-client`: `basicRender(<PanelApp context={context} provide={provide} />)`.
|
|
58
|
-
A parent `BASE` route renders the layout via `handler(Layout)`; `HOME` is its default child. Screens
|
|
59
|
-
call the API with `context.entrypoint(alias).call({ params, body })`.
|
|
60
|
-
|
|
61
|
-
This shape is auth-free by design; add `@owlmeans/server-auth` + `@owlmeans/client-auth` and
|
|
62
|
-
`guard(...)` when needed.
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Mandatory git rules — never run state-changing git without explicit instruction, commit only under the user's configured identity (never as the AI/agent, no Co-Authored-By trailer), report finished git work as a Markdown table, and never commit a conflicted working copy. Consult before any git operation."
|
|
3
|
-
applyTo: "**"
|
|
4
|
-
scope: general
|
|
5
|
-
---
|
|
6
|
-
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
-
|
|
8
|
-
# Git Workflow Rules
|
|
9
|
-
|
|
10
|
-
**Mandatory and universal.** These rules apply to every git operation in this repository and
|
|
11
|
-
**override any default agent behavior** — including any automatic `Co-Authored-By` or
|
|
12
|
-
AI/agent-attribution trailer. Consult them before running any git command.
|
|
13
|
-
|
|
14
|
-
## 0. Never run state-changing git without explicit instruction
|
|
15
|
-
|
|
16
|
-
- **Never run state-changing git operations** (`commit`, `add`/`rm` staging, `push`,
|
|
17
|
-
`reset`/rollback, `revert`, `rebase`, `merge`, `branch`, `checkout`/`switch`, `stash`,
|
|
18
|
-
`tag`, `cherry-pick`, force-push, etc.) unless the user **explicitly instructs it in the
|
|
19
|
-
current request**. Permission to make code edits is **not** permission to touch git.
|
|
20
|
-
- **Only exception**: creating and operating inside a **temporary git worktree** that a task
|
|
21
|
-
or subagent has **explicitly requested** for that purpose. Outside such an explicitly
|
|
22
|
-
requested tmp worktree, do nothing with git.
|
|
23
|
-
- **Read-only inspection is allowed**: `git status`, `git diff`, `git log`, `git show`,
|
|
24
|
-
`git branch --list`, etc. — use these to report state, never to change it.
|
|
25
|
-
|
|
26
|
-
## 1. Never commit under the agent's identity
|
|
27
|
-
|
|
28
|
-
- Commit only under the repository's preconfigured git identity — whatever `git config user.name`
|
|
29
|
-
and `git config user.email` resolve to (set by the user, globally or locally).
|
|
30
|
-
- **Never** override authorship: do not pass `--author`, do not set or change `user.name` /
|
|
31
|
-
`user.email`, and never substitute an AI / agent / assistant name or email.
|
|
32
|
-
- **Never** add a `Co-Authored-By:` trailer (or any other trailer) attributing the commit to
|
|
33
|
-
Claude, Copilot, or any AI/agent. Every commit is the user's, attributed solely to the user.
|
|
34
|
-
- If the repository has no git identity configured (neither local nor global), stop and ask the
|
|
35
|
-
user — do not invent one.
|
|
36
|
-
|
|
37
|
-
## 2. Always report finished git work as a table
|
|
38
|
-
|
|
39
|
-
- After completing any git action (commit, push, branch, checkout, merge, rebase, stash, tag,
|
|
40
|
-
reset, etc.), summarize what was done as a Markdown table.
|
|
41
|
-
- One row per action. Include at least **Action**, **Target** (branch / remote / files / ref),
|
|
42
|
-
and **Result** (commit SHA, `pushed`, `up to date`, `conflict`, …). Add a **Notes** column
|
|
43
|
-
when useful.
|
|
44
|
-
|
|
45
|
-
## 3. Never commit a conflicted working copy
|
|
46
|
-
|
|
47
|
-
- If a `merge`, `rebase`, `cherry-pick`, `stash pop`/`apply`, or `pull` produces conflicts,
|
|
48
|
-
**do not** finalize it: do not run `git commit`, `git merge --continue`,
|
|
49
|
-
`git rebase --continue`, or stage-and-commit the conflicted tree to "resolve" it on the
|
|
50
|
-
user's behalf.
|
|
51
|
-
- Stop, list the conflicted paths in the report table, and hand control back to the user —
|
|
52
|
-
unless the user has explicitly told you how to resolve the conflict and commit.
|