@soleri/core 9.14.4 → 9.16.7
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/data/flows/deliver.flow.yaml +11 -0
- package/data/flows/design.flow.yaml +4 -14
- package/data/flows/enhance.flow.yaml +10 -0
- package/data/flows/explore.flow.yaml +16 -0
- package/data/flows/fix.flow.yaml +1 -1
- package/data/flows/review.flow.yaml +13 -4
- package/dist/brain/brain.d.ts +9 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +11 -1
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +24 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/capabilities/chain-mapping.d.ts.map +1 -1
- package/dist/capabilities/chain-mapping.js +5 -4
- package/dist/capabilities/chain-mapping.js.map +1 -1
- package/dist/capabilities/registry.d.ts +6 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +3 -2
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/chat/chat-session.d.ts +6 -0
- package/dist/chat/chat-session.d.ts.map +1 -1
- package/dist/chat/chat-session.js +68 -17
- package/dist/chat/chat-session.js.map +1 -1
- package/dist/context/context-engine.js +1 -1
- package/dist/context/context-engine.js.map +1 -1
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +138 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/types.d.ts +10 -0
- package/dist/curator/types.d.ts.map +1 -1
- package/dist/engine/bin/soleri-engine.js +0 -0
- package/dist/engine/core-ops.d.ts.map +1 -1
- package/dist/engine/core-ops.js +38 -1
- package/dist/engine/core-ops.js.map +1 -1
- package/dist/flows/epilogue.d.ts +5 -1
- package/dist/flows/epilogue.d.ts.map +1 -1
- package/dist/flows/epilogue.js +11 -3
- package/dist/flows/epilogue.js.map +1 -1
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +13 -5
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +1 -2
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -0
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/plan-builder.d.ts +17 -1
- package/dist/flows/plan-builder.d.ts.map +1 -1
- package/dist/flows/plan-builder.js +67 -6
- package/dist/flows/plan-builder.js.map +1 -1
- package/dist/flows/probes.d.ts +1 -1
- package/dist/flows/probes.d.ts.map +1 -1
- package/dist/flows/probes.js +15 -3
- package/dist/flows/probes.js.map +1 -1
- package/dist/flows/types.d.ts +47 -20
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/flows/types.js +6 -1
- package/dist/flows/types.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +10 -4
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +19 -5
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +18 -0
- package/dist/intake/text-ingester.d.ts.map +1 -1
- package/dist/intake/text-ingester.js +37 -13
- package/dist/intake/text-ingester.js.map +1 -1
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +28 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +4 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +50 -4
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/playbook-executor.d.ts +10 -1
- package/dist/playbooks/playbook-executor.d.ts.map +1 -1
- package/dist/playbooks/playbook-executor.js +8 -2
- package/dist/playbooks/playbook-executor.js.map +1 -1
- package/dist/playbooks/playbook-types.d.ts +8 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +30 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +60 -21
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts +11 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +146 -37
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +38 -12
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +16 -4
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/context-facade.d.ts.map +1 -1
- package/dist/runtime/facades/context-facade.js +9 -3
- package/dist/runtime/facades/context-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -7
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +40 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +113 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +24 -3
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +21 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +132 -38
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +16 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/schema-helpers.d.ts.map +1 -1
- package/dist/runtime/schema-helpers.js +4 -0
- package/dist/runtime/schema-helpers.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +16 -3
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/scheduler/cron-validator.d.ts +15 -0
- package/dist/scheduler/cron-validator.d.ts.map +1 -0
- package/dist/scheduler/cron-validator.js +93 -0
- package/dist/scheduler/cron-validator.js.map +1 -0
- package/dist/scheduler/platform-linux.d.ts +14 -0
- package/dist/scheduler/platform-linux.d.ts.map +1 -0
- package/dist/scheduler/platform-linux.js +107 -0
- package/dist/scheduler/platform-linux.js.map +1 -0
- package/dist/scheduler/platform-macos.d.ts +15 -0
- package/dist/scheduler/platform-macos.d.ts.map +1 -0
- package/dist/scheduler/platform-macos.js +131 -0
- package/dist/scheduler/platform-macos.js.map +1 -0
- package/dist/scheduler/scheduler-ops.d.ts +14 -0
- package/dist/scheduler/scheduler-ops.d.ts.map +1 -0
- package/dist/scheduler/scheduler-ops.js +77 -0
- package/dist/scheduler/scheduler-ops.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +144 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/types.d.ts +48 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +6 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +11 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +132 -38
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/skills/validate-skills.d.ts +32 -0
- package/dist/skills/validate-skills.d.ts.map +1 -0
- package/dist/skills/validate-skills.js +396 -0
- package/dist/skills/validate-skills.js.map +1 -0
- package/dist/utils/worktree-reaper.d.ts +38 -0
- package/dist/utils/worktree-reaper.d.ts.map +1 -0
- package/dist/utils/worktree-reaper.js +85 -0
- package/dist/utils/worktree-reaper.js.map +1 -0
- package/dist/vault/default-canonical-tags.d.ts +15 -0
- package/dist/vault/default-canonical-tags.d.ts.map +1 -0
- package/dist/vault/default-canonical-tags.js +65 -0
- package/dist/vault/default-canonical-tags.js.map +1 -0
- package/dist/vault/scope-detector.d.ts.map +1 -1
- package/dist/vault/scope-detector.js +37 -4
- package/dist/vault/scope-detector.js.map +1 -1
- package/dist/vault/tag-normalizer.d.ts +42 -0
- package/dist/vault/tag-normalizer.d.ts.map +1 -0
- package/dist/vault/tag-normalizer.js +157 -0
- package/dist/vault/tag-normalizer.js.map +1 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +3 -1
- package/dist/vault/vault-entries.js.map +1 -1
- package/package.json +5 -1
- package/src/__tests__/embeddings.test.ts +3 -3
- package/src/agency/agency-manager.test.ts +4 -4
- package/src/agency/default-rules.test.ts +0 -13
- package/src/brain/brain-intelligence.test.ts +0 -5
- package/src/brain/brain.ts +25 -1
- package/src/brain/intelligence.ts +25 -0
- package/src/brain/second-brain-features.test.ts +2 -14
- package/src/brain/types.ts +1 -0
- package/src/capabilities/chain-mapping.test.ts +1 -6
- package/src/capabilities/chain-mapping.ts +6 -4
- package/src/capabilities/registry.test.ts +1 -1
- package/src/capabilities/registry.ts +9 -2
- package/src/chat/agent-loop.test.ts +1 -1
- package/src/chat/chat-enhanced.test.ts +0 -8
- package/src/chat/chat-session.ts +75 -17
- package/src/chat/chat-transport.test.ts +31 -1
- package/src/claudemd/compose.test.ts +0 -5
- package/src/context/context-engine.test.ts +0 -1
- package/src/context/context-engine.ts +1 -1
- package/src/control/intent-router.test.ts +2 -2
- package/src/curator/curator.ts +180 -0
- package/src/curator/tag-manager.test.ts +0 -4
- package/src/curator/types.ts +10 -0
- package/src/domain-packs/types.test.ts +0 -5
- package/src/dream/dream.test.ts +0 -7
- package/src/enforcement/registry.test.ts +2 -2
- package/src/engine/core-ops.test.ts +4 -22
- package/src/engine/core-ops.ts +36 -1
- package/src/engine/module-manifest.test.ts +1 -31
- package/src/engine/register-engine.test.ts +3 -33
- package/src/errors/retry.test.ts +3 -1
- package/src/flows/chain-runner.test.ts +0 -6
- package/src/flows/context-router.test.ts +3 -3
- package/src/flows/epilogue.test.ts +40 -2
- package/src/flows/epilogue.ts +11 -2
- package/src/flows/executor.test.ts +48 -2
- package/src/flows/executor.ts +15 -5
- package/src/flows/index.ts +1 -3
- package/src/flows/plan-builder.test.ts +201 -0
- package/src/flows/plan-builder.ts +81 -5
- package/src/flows/probes.ts +17 -3
- package/src/flows/types.ts +31 -2
- package/src/health/health-registry.test.ts +3 -1
- package/src/index.ts +24 -0
- package/src/intake/content-classifier.ts +22 -4
- package/src/intake/dedup-gate.test.ts +2 -6
- package/src/intake/text-ingester.test.ts +3 -4
- package/src/intake/text-ingester.ts +61 -12
- package/src/llm/llm-client.test.ts +1 -1
- package/src/llm/utils.test.ts +1 -1
- package/src/migrations/migration-runner.test.ts +0 -1
- package/src/operator/operator-context-store.test.ts +0 -13
- package/src/operator/operator-profile.test.ts +2 -20
- package/src/packs/pack-installer.ts +28 -2
- package/src/packs/pack-system.test.ts +2 -2
- package/src/persona/defaults.test.ts +19 -19
- package/src/planning/gap-passes.test.ts +0 -46
- package/src/planning/gap-patterns.test.ts +0 -42
- package/src/planning/goal-ancestry.test.ts +3 -1
- package/src/planning/plan-lifecycle.test.ts +15 -7
- package/src/planning/planner-types.ts +2 -0
- package/src/planning/planner.test.ts +86 -90
- package/src/planning/planner.ts +56 -4
- package/src/planning/reconciliation-engine.test.ts +3 -10
- package/src/planning/task-complexity-assessor.test.ts +0 -5
- package/src/planning/task-verifier.test.ts +3 -1
- package/src/playbooks/generic/generic-playbooks.test.ts +0 -28
- package/src/playbooks/index.test.ts +0 -55
- package/src/playbooks/playbook-executor.test.ts +76 -0
- package/src/playbooks/playbook-executor.ts +24 -3
- package/src/playbooks/playbook-types.ts +8 -0
- package/src/plugins/plugin-registry.test.ts +6 -2
- package/src/project/project-registry.test.ts +2 -0
- package/src/queue/async-infrastructure.test.ts +6 -4
- package/src/queue/job-queue.test.ts +13 -7
- package/src/runtime/admin-extra-ops.test.ts +35 -30
- package/src/runtime/admin-extra-ops.ts +30 -0
- package/src/runtime/admin-ops.test.ts +0 -4
- package/src/runtime/admin-ops.ts +63 -21
- package/src/runtime/admin-setup-ops.test.ts +229 -13
- package/src/runtime/admin-setup-ops.ts +145 -36
- package/src/runtime/archive-ops.test.ts +0 -28
- package/src/runtime/branching-ops.test.ts +0 -17
- package/src/runtime/capture-ops.test.ts +41 -16
- package/src/runtime/capture-ops.ts +78 -46
- package/src/runtime/chain-ops.test.ts +0 -21
- package/src/runtime/facades/admin-facade.test.ts +0 -34
- package/src/runtime/facades/agency-facade.test.ts +0 -39
- package/src/runtime/facades/archive-facade.test.ts +0 -43
- package/src/runtime/facades/brain-facade.test.ts +8 -99
- package/src/runtime/facades/brain-facade.ts +29 -12
- package/src/runtime/facades/branching-facade.test.ts +30 -17
- package/src/runtime/facades/chat-facade.test.ts +0 -91
- package/src/runtime/facades/chat-service-ops.test.ts +0 -24
- package/src/runtime/facades/chat-session-ops.test.ts +0 -12
- package/src/runtime/facades/chat-transport-ops.test.ts +0 -23
- package/src/runtime/facades/context-facade.test.ts +0 -17
- package/src/runtime/facades/context-facade.ts +11 -4
- package/src/runtime/facades/control-facade.test.ts +0 -30
- package/src/runtime/facades/curator-facade.test.ts +0 -33
- package/src/runtime/facades/intake-facade.test.ts +0 -33
- package/src/runtime/facades/links-facade.test.ts +0 -37
- package/src/runtime/facades/loop-facade.test.ts +0 -26
- package/src/runtime/facades/memory-facade.test.ts +0 -18
- package/src/runtime/facades/memory-facade.ts +27 -11
- package/src/runtime/facades/operator-facade.test.ts +0 -31
- package/src/runtime/facades/orchestrate-facade.test.ts +0 -21
- package/src/runtime/facades/orchestrate-facade.ts +39 -1
- package/src/runtime/facades/plan-facade.test.ts +7 -32
- package/src/runtime/facades/plan-facade.ts +137 -4
- package/src/runtime/facades/review-facade.test.ts +1 -49
- package/src/runtime/facades/sync-facade.test.ts +24 -41
- package/src/runtime/facades/tier-facade.test.ts +30 -22
- package/src/runtime/facades/vault-facade.test.ts +0 -41
- package/src/runtime/facades/vault-facade.ts +26 -3
- package/src/runtime/grading-ops.test.ts +0 -27
- package/src/runtime/intake-ops.test.ts +0 -19
- package/src/runtime/loop-ops.test.ts +0 -48
- package/src/runtime/memory-cross-project-ops.test.ts +0 -14
- package/src/runtime/memory-extra-ops.test.ts +4 -8
- package/src/runtime/orchestrate-ops.test.ts +238 -19
- package/src/runtime/orchestrate-ops.ts +166 -41
- package/src/runtime/pack-ops.test.ts +0 -26
- package/src/runtime/planning-extra-ops.test.ts +2 -14
- package/src/runtime/playbook-ops-execution.test.ts +9 -20
- package/src/runtime/playbook-ops.test.ts +4 -67
- package/src/runtime/review-ops.test.ts +0 -15
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/schema-helpers.ts +4 -0
- package/src/runtime/sync-ops.test.ts +0 -18
- package/src/runtime/tier-ops.test.ts +0 -21
- package/src/runtime/types.ts +19 -0
- package/src/runtime/vault-extra-ops.test.ts +0 -12
- package/src/runtime/vault-linking-ops.test.ts +0 -4
- package/src/runtime/vault-linking-ops.ts +26 -8
- package/src/runtime/vault-sharing-ops.test.ts +0 -9
- package/src/scheduler/cron-validator.ts +101 -0
- package/src/scheduler/platform-linux.ts +122 -0
- package/src/scheduler/platform-macos.ts +150 -0
- package/src/scheduler/scheduler-ops.ts +77 -0
- package/src/scheduler/scheduler.test.ts +247 -0
- package/src/scheduler/scheduler.ts +174 -0
- package/src/scheduler/types.ts +52 -0
- package/src/skills/__tests__/sync-skills.test.ts +6 -17
- package/src/skills/global-claude-md.test.ts +113 -0
- package/src/skills/sync-skills.ts +143 -35
- package/src/skills/validate-skills.test.ts +206 -0
- package/src/skills/validate-skills.ts +470 -0
- package/src/telemetry/telemetry.test.ts +1 -0
- package/src/transport/http-server.test.ts +3 -0
- package/src/transport/session-manager.test.ts +3 -1
- package/src/transport/token-auth.test.ts +6 -9
- package/src/transport/ws-server.test.ts +10 -2
- package/src/utils/worktree-reaper.ts +113 -0
- package/src/vault/__tests__/vault-characterization.test.ts +0 -108
- package/src/vault/default-canonical-tags.ts +64 -0
- package/src/vault/linking.test.ts +0 -2
- package/src/vault/playbook.test.ts +4 -1
- package/src/vault/scope-detector.test.ts +3 -1
- package/src/vault/scope-detector.ts +42 -4
- package/src/vault/tag-normalizer.test.ts +214 -0
- package/src/vault/tag-normalizer.ts +188 -0
- package/src/vault/vault-connect.test.ts +1 -1
- package/src/vault/vault-entries.ts +3 -1
- package/src/vault/vault.test.ts +23 -8
- package/dist/embeddings/index.d.ts +0 -5
- package/dist/embeddings/index.d.ts.map +0 -1
- package/dist/embeddings/index.js +0 -3
- package/dist/embeddings/index.js.map +0 -1
|
@@ -173,25 +173,11 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
|
|
|
173
173
|
expect(stat.isSymbolicLink()).toBe(true);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
-
it('
|
|
176
|
+
it('does NOT touch ~/.claude/skills/ during project-local install', async () => {
|
|
177
177
|
createSourceSkill('soleri-vault-capture');
|
|
178
|
-
//
|
|
178
|
+
// Global dir has ernesto-soleri-* entries — project-local sync must leave them alone
|
|
179
179
|
createGlobalSkillDir('ernesto-soleri-vault-capture');
|
|
180
180
|
createGlobalSkillDir('ernesto-soleri-vault-navigator');
|
|
181
|
-
|
|
182
|
-
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
183
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
|
|
184
|
-
projectRoot: fakeProject,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-capture');
|
|
188
|
-
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-navigator');
|
|
189
|
-
expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(false);
|
|
190
|
-
expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(false);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('does not clean global entries that do not match agent-soleri- prefix', async () => {
|
|
194
|
-
createSourceSkill('soleri-vault-capture');
|
|
195
181
|
createGlobalSkillDir('other-agent-skill');
|
|
196
182
|
|
|
197
183
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
@@ -199,7 +185,10 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
|
|
|
199
185
|
projectRoot: fakeProject,
|
|
200
186
|
});
|
|
201
187
|
|
|
202
|
-
|
|
188
|
+
// cleanedGlobal must be empty — project-local sync must not remove global entries
|
|
189
|
+
expect(result.cleanedGlobal).toHaveLength(0);
|
|
190
|
+
expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(true);
|
|
191
|
+
expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(true);
|
|
203
192
|
expect(globalDirExists('other-agent-skill')).toBe(true);
|
|
204
193
|
});
|
|
205
194
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for global ~/.claude/CLAUDE.md scaffolding functions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { scaffoldGlobalClaudeMd, removeAgentFromGlobalClaudeMd } from './sync-skills.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Test harness — redirect homedir() to a temp dir via env override
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
let tmpHome: string;
|
|
16
|
+
let claudeDir: string;
|
|
17
|
+
let claudeMdPath: string;
|
|
18
|
+
|
|
19
|
+
// We patch the module by temporarily pointing HOME (and USERPROFILE on Windows) at a temp dir
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpHome = join(tmpdir(), `soleri-claude-md-test-${Date.now()}`);
|
|
22
|
+
claudeDir = join(tmpHome, '.claude');
|
|
23
|
+
claudeMdPath = join(claudeDir, 'CLAUDE.md');
|
|
24
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
25
|
+
process.env['HOME'] = tmpHome;
|
|
26
|
+
process.env['USERPROFILE'] = tmpHome; // Windows: homedir() reads USERPROFILE, not HOME
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
31
|
+
delete process.env['HOME'];
|
|
32
|
+
delete process.env['USERPROFILE'];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('scaffoldGlobalClaudeMd', () => {
|
|
36
|
+
it('creates CLAUDE.md with header and agent section when file does not exist', () => {
|
|
37
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
38
|
+
|
|
39
|
+
expect(existsSync(claudeMdPath)).toBe(true);
|
|
40
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
41
|
+
expect(content).toContain('# Soleri Engine');
|
|
42
|
+
expect(content).toContain('<!-- soleri:agent:ernesto start -->');
|
|
43
|
+
expect(content).toContain('<!-- soleri:agent:ernesto end -->');
|
|
44
|
+
expect(content).toContain('## Ernesto');
|
|
45
|
+
expect(content).toContain('ernesto_*');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('replaces existing agent section on second call (idempotent)', () => {
|
|
49
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
50
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
51
|
+
|
|
52
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
53
|
+
const startCount = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
|
|
54
|
+
expect(startCount).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('appends a second agent section without disturbing the first', () => {
|
|
58
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
59
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
60
|
+
|
|
61
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
62
|
+
expect(content).toContain('<!-- soleri:agent:ernesto start -->');
|
|
63
|
+
expect(content).toContain('<!-- soleri:agent:ernesto end -->');
|
|
64
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
65
|
+
expect(content).toContain('<!-- soleri:agent:salvador end -->');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('updating one agent does not affect another agent section', () => {
|
|
69
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
70
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
71
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto Updated');
|
|
72
|
+
|
|
73
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
74
|
+
expect(content).toContain('Ernesto Updated');
|
|
75
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
76
|
+
// Only one ernesto section
|
|
77
|
+
const count = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
|
|
78
|
+
expect(count).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('removeAgentFromGlobalClaudeMd', () => {
|
|
83
|
+
it('is a no-op when CLAUDE.md does not exist', () => {
|
|
84
|
+
expect(() => removeAgentFromGlobalClaudeMd('ernesto')).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('removes the agent section', () => {
|
|
88
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
89
|
+
removeAgentFromGlobalClaudeMd('ernesto');
|
|
90
|
+
|
|
91
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
92
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
|
|
93
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto end -->');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('only removes the target agent, leaving others intact', () => {
|
|
97
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
98
|
+
scaffoldGlobalClaudeMd('salvador', 'Salvador');
|
|
99
|
+
removeAgentFromGlobalClaudeMd('ernesto');
|
|
100
|
+
|
|
101
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
102
|
+
expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
|
|
103
|
+
expect(content).toContain('<!-- soleri:agent:salvador start -->');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('is a no-op when the agent section is not in the file', () => {
|
|
107
|
+
scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
|
|
108
|
+
const before = readFileSync(claudeMdPath, 'utf-8');
|
|
109
|
+
removeAgentFromGlobalClaudeMd('nonexistent');
|
|
110
|
+
const after = readFileSync(claudeMdPath, 'utf-8');
|
|
111
|
+
expect(after).toBe(before);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -82,7 +82,18 @@ export function discoverSkills(skillsDirs: string[]): SkillEntry[] {
|
|
|
82
82
|
if (!existsSync(dir)) continue;
|
|
83
83
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
84
84
|
for (const entry of entries) {
|
|
85
|
-
|
|
85
|
+
// Follow symlinks — project-local installs use symlinks to source skills
|
|
86
|
+
const isDir =
|
|
87
|
+
entry.isDirectory() ||
|
|
88
|
+
(entry.isSymbolicLink() &&
|
|
89
|
+
(() => {
|
|
90
|
+
try {
|
|
91
|
+
return statSync(join(dir, entry.name)).isDirectory();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
})());
|
|
96
|
+
if (!isDir) continue;
|
|
86
97
|
const skillPath = join(dir, entry.name, 'SKILL.md');
|
|
87
98
|
if (existsSync(skillPath)) {
|
|
88
99
|
skills.push({ name: entry.name, sourcePath: skillPath });
|
|
@@ -248,51 +259,148 @@ export function syncSkillsToClaudeCode(
|
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
251
|
-
// Task 3:
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
// Task 3: (removed — cleanStaleGlobalSkills was wiping valid ernesto-soleri-* entries
|
|
263
|
+
// that the global sync installed. Task 2 orphan removal handles stale entries during
|
|
264
|
+
// global sync; project-local sync must not touch ~/.claude/skills/.)
|
|
265
|
+
|
|
266
|
+
// Task 4: Scaffold global ~/.claude/CLAUDE.md when doing a global install
|
|
267
|
+
if (isGlobal && agentName && result.installed.length + result.updated.length > 0) {
|
|
268
|
+
try {
|
|
269
|
+
const agentId = agentName.toLowerCase().replace(/\s+/g, '-');
|
|
270
|
+
const displayName = agentName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
271
|
+
scaffoldGlobalClaudeMd(agentId, displayName);
|
|
272
|
+
} catch {
|
|
273
|
+
// Best-effort — don't fail the sync
|
|
274
|
+
}
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
return result;
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// GLOBAL CLAUDE.MD SCAFFOLDING
|
|
282
|
+
// =============================================================================
|
|
283
|
+
|
|
259
284
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* Cleans entries from ALL agents, not just the current one — any
|
|
264
|
-
* `*-soleri-*` entry in the global dir is a stale copy from a previous
|
|
265
|
-
* global install. Canonical skills now live in project-local .claude/skills/.
|
|
285
|
+
* Sentinel markers that bracket an agent's section in ~/.claude/CLAUDE.md.
|
|
286
|
+
* Using HTML comments so they don't render in markdown viewers.
|
|
266
287
|
*/
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
function agentSectionStart(agentId: string): string {
|
|
289
|
+
return `<!-- soleri:agent:${agentId} start -->`;
|
|
290
|
+
}
|
|
291
|
+
function agentSectionEnd(agentId: string): string {
|
|
292
|
+
return `<!-- soleri:agent:${agentId} end -->`;
|
|
293
|
+
}
|
|
270
294
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Build the minimal routing section for one agent in the global CLAUDE.md.
|
|
297
|
+
*/
|
|
298
|
+
function buildAgentSection(agentId: string, displayName: string): string {
|
|
299
|
+
return [
|
|
300
|
+
agentSectionStart(agentId),
|
|
301
|
+
`## ${displayName}`,
|
|
302
|
+
'',
|
|
303
|
+
`Skills for **${displayName}** are installed globally. Agent-specific instructions`,
|
|
304
|
+
`are in each project's \`CLAUDE.md\`.`,
|
|
305
|
+
'',
|
|
306
|
+
`**Routing:** When you see \`${agentId}_*\` MCP tools, follow the project's \`CLAUDE.md\`.`,
|
|
307
|
+
agentSectionEnd(agentId),
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
274
310
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Create or update ~/.claude/CLAUDE.md to include a routing section for the
|
|
313
|
+
* given agent. Idempotent — replaces the agent's existing section if present,
|
|
314
|
+
* appends otherwise. Does not touch other agents' sections.
|
|
315
|
+
*/
|
|
316
|
+
export function scaffoldGlobalClaudeMd(agentId: string, displayName: string): void {
|
|
317
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
318
|
+
|
|
319
|
+
const header = [
|
|
320
|
+
'# Soleri Engine',
|
|
321
|
+
'',
|
|
322
|
+
'Active Soleri agents are installed on this system. Agent-specific instructions',
|
|
323
|
+
"are in each project's `CLAUDE.md`.",
|
|
324
|
+
'',
|
|
325
|
+
'## Routing',
|
|
326
|
+
'',
|
|
327
|
+
"- When working in a Soleri agent project, follow that project's `CLAUDE.md`",
|
|
328
|
+
'- Agent sections below are managed automatically — do not edit manually',
|
|
329
|
+
'',
|
|
330
|
+
].join('\n');
|
|
331
|
+
|
|
332
|
+
const newSection = buildAgentSection(agentId, displayName);
|
|
333
|
+
const start = agentSectionStart(agentId);
|
|
334
|
+
const end = agentSectionEnd(agentId);
|
|
335
|
+
|
|
336
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
337
|
+
|
|
338
|
+
let existing = '';
|
|
339
|
+
if (existsSync(claudeMdPath)) {
|
|
340
|
+
existing = readFileSync(claudeMdPath, 'utf-8');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let startIdx = existing.indexOf(start);
|
|
344
|
+
let endIdx = existing.indexOf(end);
|
|
345
|
+
|
|
346
|
+
// Migrate legacy sentinel format: <!-- agent:{id}:mode --> / <!-- /agent:{id}:mode -->
|
|
347
|
+
if (startIdx === -1) {
|
|
348
|
+
const legacyStart = `<!-- agent:${agentId}:mode -->`;
|
|
349
|
+
const legacyEnd = `<!-- /agent:${agentId}:mode -->`;
|
|
350
|
+
const ls = existing.indexOf(legacyStart);
|
|
351
|
+
const le = existing.indexOf(legacyEnd);
|
|
352
|
+
if (ls !== -1 && le !== -1) {
|
|
353
|
+
startIdx = ls;
|
|
354
|
+
endIdx = le;
|
|
355
|
+
// Point past the legacy end sentinel so we replace the whole block
|
|
356
|
+
existing =
|
|
357
|
+
existing.slice(0, ls) +
|
|
358
|
+
start +
|
|
359
|
+
existing.slice(ls + legacyStart.length, le) +
|
|
360
|
+
end +
|
|
361
|
+
existing.slice(le + legacyEnd.length);
|
|
362
|
+
startIdx = existing.indexOf(start);
|
|
363
|
+
endIdx = existing.indexOf(end);
|
|
292
364
|
}
|
|
293
|
-
} catch {
|
|
294
|
-
// Global skills dir unreadable — nothing to clean
|
|
295
365
|
}
|
|
366
|
+
|
|
367
|
+
let updated: string;
|
|
368
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
369
|
+
// Replace the existing section
|
|
370
|
+
updated = existing.slice(0, startIdx) + newSection + existing.slice(endIdx + end.length);
|
|
371
|
+
} else if (existing.length === 0) {
|
|
372
|
+
// New file — write header + section
|
|
373
|
+
updated = header + newSection + '\n';
|
|
374
|
+
} else {
|
|
375
|
+
// Append to existing file
|
|
376
|
+
updated = existing.trimEnd() + '\n\n' + newSection + '\n';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
writeFileSync(claudeMdPath, updated);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Remove an agent's section from ~/.claude/CLAUDE.md.
|
|
384
|
+
* No-op if the file or section doesn't exist.
|
|
385
|
+
*/
|
|
386
|
+
export function removeAgentFromGlobalClaudeMd(agentId: string): void {
|
|
387
|
+
const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
|
|
388
|
+
if (!existsSync(claudeMdPath)) return;
|
|
389
|
+
|
|
390
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
391
|
+
const start = agentSectionStart(agentId);
|
|
392
|
+
const end = agentSectionEnd(agentId);
|
|
393
|
+
|
|
394
|
+
const startIdx = content.indexOf(start);
|
|
395
|
+
const endIdx = content.indexOf(end);
|
|
396
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
397
|
+
|
|
398
|
+
// Remove the section and any leading blank line before it
|
|
399
|
+
const before = content.slice(0, startIdx).trimEnd();
|
|
400
|
+
const after = content.slice(endIdx + end.length);
|
|
401
|
+
|
|
402
|
+
const updated = (before.length > 0 ? before + '\n' : '') + after.replace(/^\n+/, '\n');
|
|
403
|
+
writeFileSync(claudeMdPath, updated.trimEnd() + '\n');
|
|
296
404
|
}
|
|
297
405
|
|
|
298
406
|
// =============================================================================
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for validate-skills — the user-installed SKILL.md validator.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { validateSkillDocs } from './validate-skills.js';
|
|
10
|
+
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function createSkillsDir(): string {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), 'soleri-validate-skills-test-'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addSkill(skillsDir: string, skillName: string, content: string): void {
|
|
18
|
+
const skillDir = join(skillsDir, skillName);
|
|
19
|
+
mkdirSync(skillDir, { recursive: true });
|
|
20
|
+
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Tests ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe('validateSkillDocs', () => {
|
|
26
|
+
let skillsDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
skillsDir = createSkillsDir();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(skillsDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns valid=true and no errors when the skills directory is empty', () => {
|
|
37
|
+
const result = validateSkillDocs(skillsDir);
|
|
38
|
+
expect(result.valid).toBe(true);
|
|
39
|
+
expect(result.errors).toHaveLength(0);
|
|
40
|
+
expect(result.totalFiles).toBe(0);
|
|
41
|
+
expect(result.totalExamples).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns valid=true for a SKILL.md with no op-call examples', () => {
|
|
45
|
+
addSkill(
|
|
46
|
+
skillsDir,
|
|
47
|
+
'my-skill',
|
|
48
|
+
`# My Skill
|
|
49
|
+
|
|
50
|
+
This skill does something useful.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Just invoke it.
|
|
55
|
+
`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = validateSkillDocs(skillsDir);
|
|
59
|
+
expect(result.valid).toBe(true);
|
|
60
|
+
expect(result.errors).toHaveLength(0);
|
|
61
|
+
expect(result.totalFiles).toBe(1);
|
|
62
|
+
expect(result.totalExamples).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns valid=true when op-call params match the schema', () => {
|
|
66
|
+
addSkill(
|
|
67
|
+
skillsDir,
|
|
68
|
+
'capture-skill',
|
|
69
|
+
`# Capture Skill
|
|
70
|
+
|
|
71
|
+
Captures knowledge to the vault.
|
|
72
|
+
|
|
73
|
+
\`\`\`
|
|
74
|
+
YOUR_AGENT_core op:capture_knowledge params: { projectPath: ".", entries: [{ type: "pattern", domain: "testing", title: "Use vitest", description: "Prefer vitest for unit tests", severity: "info" }] }
|
|
75
|
+
\`\`\`
|
|
76
|
+
`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = validateSkillDocs(skillsDir);
|
|
80
|
+
expect(result.valid).toBe(true);
|
|
81
|
+
expect(result.errors).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reports an error when severity has an invalid enum value', () => {
|
|
85
|
+
// "suggestion" is not in the capture_knowledge severity enum (valid: critical, warning, info)
|
|
86
|
+
addSkill(
|
|
87
|
+
skillsDir,
|
|
88
|
+
'bad-severity-skill',
|
|
89
|
+
`# Bad Skill
|
|
90
|
+
|
|
91
|
+
Example with wrong severity enum:
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
95
|
+
\`\`\`
|
|
96
|
+
`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = validateSkillDocs(skillsDir);
|
|
100
|
+
expect(result.valid).toBe(false);
|
|
101
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
102
|
+
|
|
103
|
+
const severityError = result.errors.find(
|
|
104
|
+
(e) => e.op === 'capture_knowledge' && e.message.toLowerCase().includes('invalid'),
|
|
105
|
+
);
|
|
106
|
+
expect(severityError).toBeDefined();
|
|
107
|
+
expect(severityError!.file).toContain('bad-severity-skill');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reports an error when scope receives an object instead of a string', () => {
|
|
111
|
+
// create_plan scope expects z.string() but we pass an object
|
|
112
|
+
addSkill(
|
|
113
|
+
skillsDir,
|
|
114
|
+
'bad-scope-skill',
|
|
115
|
+
`# Bad Scope Skill
|
|
116
|
+
|
|
117
|
+
Example with wrong scope type:
|
|
118
|
+
|
|
119
|
+
\`\`\`
|
|
120
|
+
YOUR_AGENT_core op:create_plan params: { title: "My Plan", objective: "Do something", scope: { included: [] } }
|
|
121
|
+
\`\`\`
|
|
122
|
+
`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const result = validateSkillDocs(skillsDir);
|
|
126
|
+
expect(result.valid).toBe(false);
|
|
127
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
128
|
+
|
|
129
|
+
const scopeError = result.errors.find(
|
|
130
|
+
(e) => e.op === 'create_plan' && e.message.includes('scope'),
|
|
131
|
+
);
|
|
132
|
+
expect(scopeError).toBeDefined();
|
|
133
|
+
expect(scopeError!.message).toContain('Expected string');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns structured error objects with required fields', () => {
|
|
137
|
+
addSkill(
|
|
138
|
+
skillsDir,
|
|
139
|
+
'structured-error-skill',
|
|
140
|
+
`# Structured Error Skill
|
|
141
|
+
|
|
142
|
+
\`\`\`
|
|
143
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
144
|
+
\`\`\`
|
|
145
|
+
`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = validateSkillDocs(skillsDir);
|
|
149
|
+
|
|
150
|
+
// "suggestion" is not a valid severity — expect at least one error
|
|
151
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
152
|
+
const err = result.errors[0];
|
|
153
|
+
expect(err).toHaveProperty('file');
|
|
154
|
+
expect(err).toHaveProperty('op');
|
|
155
|
+
expect(err).toHaveProperty('message');
|
|
156
|
+
expect(typeof err.file).toBe('string');
|
|
157
|
+
expect(typeof err.op).toBe('string');
|
|
158
|
+
expect(typeof err.message).toBe('string');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('includes the file path and op name in each error', () => {
|
|
162
|
+
addSkill(
|
|
163
|
+
skillsDir,
|
|
164
|
+
'named-skill',
|
|
165
|
+
`# Named Skill
|
|
166
|
+
|
|
167
|
+
\`\`\`
|
|
168
|
+
YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
|
|
169
|
+
\`\`\`
|
|
170
|
+
`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const result = validateSkillDocs(skillsDir);
|
|
174
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
175
|
+
|
|
176
|
+
const err = result.errors[0];
|
|
177
|
+
expect(err.file).toContain('named-skill');
|
|
178
|
+
expect(err.op).toBe('capture_knowledge');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('builds a schema registry covering core ops', () => {
|
|
182
|
+
const result = validateSkillDocs(skillsDir);
|
|
183
|
+
// Registry must cover: capture_knowledge, capture_quick, create_plan, approve_plan, etc.
|
|
184
|
+
expect(result.registrySize).toBeGreaterThanOrEqual(60);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('handles a skills directory that does not exist', () => {
|
|
188
|
+
const nonExistentDir = join(skillsDir, 'does-not-exist');
|
|
189
|
+
const result = validateSkillDocs(nonExistentDir);
|
|
190
|
+
expect(result.valid).toBe(true);
|
|
191
|
+
expect(result.totalFiles).toBe(0);
|
|
192
|
+
expect(result.errors).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('counts multiple skill files correctly', () => {
|
|
196
|
+
addSkill(
|
|
197
|
+
skillsDir,
|
|
198
|
+
'skill-one',
|
|
199
|
+
`# Skill One\n\n\`\`\`\nYOUR_AGENT_core op:capture_quick params: { title: "Test", content: "Content" }\n\`\`\`\n`,
|
|
200
|
+
);
|
|
201
|
+
addSkill(skillsDir, 'skill-two', `# Skill Two\n\nNo examples here.\n`);
|
|
202
|
+
|
|
203
|
+
const result = validateSkillDocs(skillsDir);
|
|
204
|
+
expect(result.totalFiles).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
});
|