@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.
Files changed (77) hide show
  1. package/README.md +46 -18
  2. package/agent-meta/manifest.json +15 -99
  3. package/agent-meta/skills/agent-memory/SKILL.md +8 -7
  4. package/agent-meta/skills/getting-started/SKILL.md +2 -1
  5. package/agent-meta/skills/git/SKILL.md +2 -1
  6. package/agent-meta/skills/memory-promotion/SKILL.md +5 -4
  7. package/agent-meta/skills/memory-recompact/SKILL.md +3 -3
  8. package/agent-meta/skills/reuse-code/SKILL.md +4 -3
  9. package/agent-meta/skills/router-plugins/SKILL.md +2 -1
  10. package/agent-meta/skills/scaffolding/SKILL.md +15 -13
  11. package/agent-meta/skills/self-education/SKILL.md +10 -10
  12. package/agent-meta/skills/shadcn-versions/SKILL.md +3 -2
  13. package/agent-meta/skills/shadcn-web/SKILL.md +2 -1
  14. package/agent-meta/skills/skill-authoring/SKILL.md +44 -39
  15. package/build/apply.d.ts +2 -1
  16. package/build/apply.d.ts.map +1 -1
  17. package/build/apply.js +40 -4
  18. package/build/apply.js.map +1 -1
  19. package/build/args.d.ts +4 -2
  20. package/build/args.d.ts.map +1 -1
  21. package/build/args.js +6 -7
  22. package/build/args.js.map +1 -1
  23. package/build/discover.d.ts +8 -5
  24. package/build/discover.d.ts.map +1 -1
  25. package/build/discover.js +13 -11
  26. package/build/discover.js.map +1 -1
  27. package/build/llm/index.d.ts +13 -0
  28. package/build/llm/index.d.ts.map +1 -0
  29. package/build/llm/index.js +13 -0
  30. package/build/llm/index.js.map +1 -0
  31. package/build/llm/manifest.d.ts +30 -0
  32. package/build/llm/manifest.d.ts.map +1 -0
  33. package/build/llm/manifest.js +59 -0
  34. package/build/llm/manifest.js.map +1 -0
  35. package/build/llm/plugin.d.ts +17 -0
  36. package/build/llm/plugin.d.ts.map +1 -0
  37. package/build/llm/plugin.js +115 -0
  38. package/build/llm/plugin.js.map +1 -0
  39. package/build/llm/resolve.d.ts +4 -0
  40. package/build/llm/resolve.d.ts.map +1 -0
  41. package/build/llm/resolve.js +145 -0
  42. package/build/llm/resolve.js.map +1 -0
  43. package/build/llm/types.d.ts +59 -0
  44. package/build/llm/types.d.ts.map +1 -0
  45. package/build/llm/types.js +3 -0
  46. package/build/llm/types.js.map +1 -0
  47. package/build/plan.d.ts.map +1 -1
  48. package/build/plan.js +7 -6
  49. package/build/plan.js.map +1 -1
  50. package/build/run.d.ts.map +1 -1
  51. package/build/run.js +9 -12
  52. package/build/run.js.map +1 -1
  53. package/package.json +23 -1
  54. package/src/apply.ts +41 -4
  55. package/src/args.ts +12 -9
  56. package/src/discover.ts +13 -14
  57. package/src/llm/index.ts +13 -0
  58. package/src/llm/manifest.ts +75 -0
  59. package/src/llm/plugin.ts +133 -0
  60. package/src/llm/resolve.ts +180 -0
  61. package/src/llm/types.ts +71 -0
  62. package/src/plan.ts +8 -6
  63. package/src/run.ts +8 -11
  64. package/tests/installer.test.ts +61 -45
  65. package/tests/llm-plugin.test.ts +158 -0
  66. package/agent-meta/instructions/agent-memory.instructions.md +0 -143
  67. package/agent-meta/instructions/getting-started.instructions.md +0 -62
  68. package/agent-meta/instructions/git.instructions.md +0 -52
  69. package/agent-meta/instructions/memory-promotion.instructions.md +0 -111
  70. package/agent-meta/instructions/memory-recompact.instructions.md +0 -73
  71. package/agent-meta/instructions/reuse-code.instructions.md +0 -50
  72. package/agent-meta/instructions/router-plugins.instructions.md +0 -46
  73. package/agent-meta/instructions/scaffolding.instructions.md +0 -68
  74. package/agent-meta/instructions/self-education.instructions.md +0 -85
  75. package/agent-meta/instructions/shadcn-versions.instructions.md +0 -47
  76. package/agent-meta/instructions/shadcn-web.instructions.md +0 -95
  77. package/agent-meta/instructions/skill-authoring.instructions.md +0 -66
@@ -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: 1,
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
- ? `.claude/skills/${e.name}/SKILL.md`
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 entries from node_modules', () => {
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(2)
100
- expect(entries.some(e => e.kind === 'skill' && e.name === 'context')).toBe(true)
101
- expect(entries.some(e => e.kind === 'instruction' && e.name === 'context')).toBe(true)
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 (kind, name), keeping highest version', () => {
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.kind === 'skill' && e.name === 'web-panel')).toBe(true)
157
- expect(entries.some(e => e.kind === 'instruction' && e.name === 'web-panel')).toBe(true)
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, '.claude', 'skills', 'context')
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, '.claude', 'skills', 'context')
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, '.claude', 'skills', 'context')
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, '.claude', 'skills', 'context')
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(2)
314
- expect(existsSync(join(tmpDir, '.claude', 'skills', 'context', 'SKILL.md'))).toBe(true)
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, '.claude', 'skills', 'context')
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.