@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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator for user-installed SKILL.md op-call examples.
|
|
3
|
+
*
|
|
4
|
+
* Reads all SKILL.md files from a given skills directory (e.g. ~/.claude/skills/),
|
|
5
|
+
* extracts inline op-call examples, and validates their params against the actual
|
|
6
|
+
* Zod schemas from the facade layer.
|
|
7
|
+
*
|
|
8
|
+
* Returns structured results rather than printing or exiting — the CLI layer owns I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import type { ZodType, ZodError } from 'zod';
|
|
14
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
15
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
16
|
+
|
|
17
|
+
// ── Facade factory imports ──────────────────────────────────────────────
|
|
18
|
+
import { createVaultFacadeOps } from '../runtime/facades/vault-facade.js';
|
|
19
|
+
import { createPlanFacadeOps } from '../runtime/facades/plan-facade.js';
|
|
20
|
+
import { createBrainFacadeOps } from '../runtime/facades/brain-facade.js';
|
|
21
|
+
import { createMemoryFacadeOps } from '../runtime/facades/memory-facade.js';
|
|
22
|
+
import { createAdminFacadeOps } from '../runtime/facades/admin-facade.js';
|
|
23
|
+
import { createCuratorFacadeOps } from '../runtime/facades/curator-facade.js';
|
|
24
|
+
import { createLoopFacadeOps } from '../runtime/facades/loop-facade.js';
|
|
25
|
+
import { createOrchestrateFacadeOps } from '../runtime/facades/orchestrate-facade.js';
|
|
26
|
+
import { createControlFacadeOps } from '../runtime/facades/control-facade.js';
|
|
27
|
+
import { createContextFacadeOps } from '../runtime/facades/context-facade.js';
|
|
28
|
+
import { createAgencyFacadeOps } from '../runtime/facades/agency-facade.js';
|
|
29
|
+
import { createChatFacadeOps } from '../runtime/facades/chat-facade.js';
|
|
30
|
+
import { createOperatorFacadeOps } from '../runtime/facades/operator-facade.js';
|
|
31
|
+
import { createArchiveFacadeOps } from '../runtime/facades/archive-facade.js';
|
|
32
|
+
import { createSyncFacadeOps } from '../runtime/facades/sync-facade.js';
|
|
33
|
+
import { createReviewFacadeOps } from '../runtime/facades/review-facade.js';
|
|
34
|
+
import { createIntakeFacadeOps } from '../runtime/facades/intake-facade.js';
|
|
35
|
+
import { createLinksFacadeOps } from '../runtime/facades/links-facade.js';
|
|
36
|
+
import { createBranchingFacadeOps } from '../runtime/facades/branching-facade.js';
|
|
37
|
+
import { createTierFacadeOps } from '../runtime/facades/tier-facade.js';
|
|
38
|
+
import { createEmbeddingFacadeOps } from '../runtime/facades/embedding-facade.js';
|
|
39
|
+
|
|
40
|
+
// ── Public types ────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface SkillValidationError {
|
|
43
|
+
file: string;
|
|
44
|
+
op: string;
|
|
45
|
+
message: string;
|
|
46
|
+
line?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SkillValidationResult {
|
|
50
|
+
valid: boolean;
|
|
51
|
+
errors: SkillValidationError[];
|
|
52
|
+
totalFiles: number;
|
|
53
|
+
totalExamples: number;
|
|
54
|
+
registrySize: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Internal types ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
interface OpExample {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
opName: string;
|
|
63
|
+
rawParams: string;
|
|
64
|
+
parsedParams: Record<string, unknown> | null;
|
|
65
|
+
parseError?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Mock runtime ────────────────────────────────────────────────────────
|
|
69
|
+
// Schemas are constructed during factory calls but handlers are never invoked.
|
|
70
|
+
|
|
71
|
+
function createNoopProxy(): AgentRuntime {
|
|
72
|
+
const handler: ProxyHandler<object> = {
|
|
73
|
+
get(_target, prop) {
|
|
74
|
+
if (prop === Symbol.toPrimitive) return () => '';
|
|
75
|
+
if (prop === Symbol.iterator) return undefined;
|
|
76
|
+
if (prop === 'then') return undefined;
|
|
77
|
+
if (prop === 'toString') return () => '[mock]';
|
|
78
|
+
if (prop === 'valueOf') return () => 0;
|
|
79
|
+
if (prop === 'length') return 0;
|
|
80
|
+
return new Proxy(function () {}, handler);
|
|
81
|
+
},
|
|
82
|
+
apply() {
|
|
83
|
+
return new Proxy({}, handler);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return new Proxy({}, handler) as unknown as AgentRuntime;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Schema registry ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function buildSchemaRegistry(): Map<string, ZodType> {
|
|
92
|
+
const runtime = createNoopProxy();
|
|
93
|
+
const registry = new Map<string, ZodType>();
|
|
94
|
+
|
|
95
|
+
const facadeFactories: Array<(rt: AgentRuntime) => OpDefinition[]> = [
|
|
96
|
+
createVaultFacadeOps,
|
|
97
|
+
createPlanFacadeOps,
|
|
98
|
+
createBrainFacadeOps,
|
|
99
|
+
createMemoryFacadeOps,
|
|
100
|
+
createAdminFacadeOps,
|
|
101
|
+
createCuratorFacadeOps,
|
|
102
|
+
createLoopFacadeOps,
|
|
103
|
+
createOrchestrateFacadeOps,
|
|
104
|
+
createControlFacadeOps,
|
|
105
|
+
createContextFacadeOps,
|
|
106
|
+
createAgencyFacadeOps,
|
|
107
|
+
createChatFacadeOps,
|
|
108
|
+
createOperatorFacadeOps,
|
|
109
|
+
createArchiveFacadeOps,
|
|
110
|
+
createSyncFacadeOps,
|
|
111
|
+
createReviewFacadeOps,
|
|
112
|
+
createIntakeFacadeOps,
|
|
113
|
+
createLinksFacadeOps,
|
|
114
|
+
createBranchingFacadeOps,
|
|
115
|
+
createTierFacadeOps,
|
|
116
|
+
createEmbeddingFacadeOps,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const factory of facadeFactories) {
|
|
120
|
+
try {
|
|
121
|
+
const ops = factory(runtime);
|
|
122
|
+
for (const op of ops) {
|
|
123
|
+
if (op.schema) {
|
|
124
|
+
registry.set(op.name, op.schema);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Some facades may fail with the mock runtime — skip them.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return registry;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── SKILL.md discovery ──────────────────────────────────────────────────
|
|
136
|
+
// Discovers all SKILL.md files directly inside skillsDir.
|
|
137
|
+
// Supports both layouts:
|
|
138
|
+
// - skillsDir/{name}/SKILL.md (directory layout)
|
|
139
|
+
// - skillsDir/{name}.md (flat file layout)
|
|
140
|
+
|
|
141
|
+
function discoverSkillFiles(skillsDir: string): string[] {
|
|
142
|
+
const paths: string[] = [];
|
|
143
|
+
|
|
144
|
+
if (!existsSync(skillsDir)) return paths;
|
|
145
|
+
|
|
146
|
+
let entries: string[];
|
|
147
|
+
try {
|
|
148
|
+
entries = readdirSync(skillsDir);
|
|
149
|
+
} catch {
|
|
150
|
+
return paths;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const entryPath = join(skillsDir, entry);
|
|
155
|
+
try {
|
|
156
|
+
const stat = statSync(entryPath);
|
|
157
|
+
if (stat.isDirectory()) {
|
|
158
|
+
const skillMd = join(entryPath, 'SKILL.md');
|
|
159
|
+
if (existsSync(skillMd)) {
|
|
160
|
+
paths.push(skillMd);
|
|
161
|
+
}
|
|
162
|
+
} else if (entry.endsWith('.md')) {
|
|
163
|
+
paths.push(entryPath);
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Skip unreadable entries
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return paths;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── SKILL.md parser ─────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function extractOpExamples(filePath: string): OpExample[] {
|
|
176
|
+
let content: string;
|
|
177
|
+
try {
|
|
178
|
+
content = readFileSync(filePath, 'utf-8');
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
const examples: OpExample[] = [];
|
|
185
|
+
|
|
186
|
+
let inCodeBlock = false;
|
|
187
|
+
let codeBlockStart = -1;
|
|
188
|
+
let codeBlockLines: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
if (line.trimStart().startsWith('```')) {
|
|
193
|
+
if (!inCodeBlock) {
|
|
194
|
+
inCodeBlock = true;
|
|
195
|
+
codeBlockStart = i + 1;
|
|
196
|
+
codeBlockLines = [];
|
|
197
|
+
} else {
|
|
198
|
+
extractFromCodeBlock(filePath, codeBlockStart, codeBlockLines, examples);
|
|
199
|
+
inCodeBlock = false;
|
|
200
|
+
codeBlockLines = [];
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (inCodeBlock) {
|
|
205
|
+
codeBlockLines.push(line);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return examples;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractFromCodeBlock(
|
|
213
|
+
filePath: string,
|
|
214
|
+
startLine: number,
|
|
215
|
+
blockLines: string[],
|
|
216
|
+
results: OpExample[],
|
|
217
|
+
): void {
|
|
218
|
+
const opPattern = /(?:YOUR_AGENT_\w+\s+)?op:(\w+)(?:\s+params:\s*(.*))?/;
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < blockLines.length; i++) {
|
|
221
|
+
const line = blockLines[i];
|
|
222
|
+
const match = line.match(opPattern);
|
|
223
|
+
if (!match) continue;
|
|
224
|
+
|
|
225
|
+
const opName = match[1];
|
|
226
|
+
const lineNum = startLine + i + 1;
|
|
227
|
+
|
|
228
|
+
let rawParams = '';
|
|
229
|
+
|
|
230
|
+
if (match[2]) {
|
|
231
|
+
rawParams = match[2].trim();
|
|
232
|
+
} else if (i + 1 < blockLines.length && blockLines[i + 1].trim().startsWith('params:')) {
|
|
233
|
+
const paramsLine = blockLines[i + 1].trim();
|
|
234
|
+
const paramsMatch = paramsLine.match(/^params:\s*(.*)/);
|
|
235
|
+
if (paramsMatch) {
|
|
236
|
+
rawParams = paramsMatch[1].trim();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (rawParams) {
|
|
241
|
+
const fullParams = collectMultiLineParams(blockLines, i, rawParams);
|
|
242
|
+
const { parsed, error } = parseLooseJson(fullParams);
|
|
243
|
+
|
|
244
|
+
results.push({
|
|
245
|
+
file: filePath,
|
|
246
|
+
line: lineNum,
|
|
247
|
+
opName,
|
|
248
|
+
rawParams: fullParams,
|
|
249
|
+
parsedParams: parsed,
|
|
250
|
+
parseError: error,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function collectMultiLineParams(blockLines: string[], opLineIdx: number, initial: string): string {
|
|
257
|
+
if (isBalanced(initial)) return initial;
|
|
258
|
+
|
|
259
|
+
let startIdx = opLineIdx + 1;
|
|
260
|
+
if (!blockLines[opLineIdx].includes('params:') && startIdx < blockLines.length) {
|
|
261
|
+
if (blockLines[startIdx].trim().startsWith('params:')) {
|
|
262
|
+
startIdx++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let result = initial;
|
|
267
|
+
for (let j = startIdx; j < blockLines.length; j++) {
|
|
268
|
+
const nextLine = blockLines[j].trim();
|
|
269
|
+
if (!nextLine) continue;
|
|
270
|
+
if (nextLine.match(/(?:YOUR_AGENT_\w+\s+)?op:\w+/)) break;
|
|
271
|
+
result += '\n' + nextLine;
|
|
272
|
+
if (isBalanced(result)) break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isBalanced(s: string): boolean {
|
|
279
|
+
let depth = 0;
|
|
280
|
+
for (const ch of s) {
|
|
281
|
+
if (ch === '{' || ch === '[') depth++;
|
|
282
|
+
if (ch === '}' || ch === ']') depth--;
|
|
283
|
+
if (depth < 0) return false;
|
|
284
|
+
}
|
|
285
|
+
return depth === 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseLooseJson(raw: string): {
|
|
289
|
+
parsed: Record<string, unknown> | null;
|
|
290
|
+
error?: string;
|
|
291
|
+
} {
|
|
292
|
+
if (!raw.trim()) return { parsed: null };
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const normalized = raw
|
|
296
|
+
.replace(/"<([^">]+)>"/g, '"$1"')
|
|
297
|
+
.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":')
|
|
298
|
+
.replace(/'/g, '"')
|
|
299
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
300
|
+
.replace(/\.\.\./g, '')
|
|
301
|
+
.replace(/\["?<[^>]+>"?\]/g, '["placeholder"]');
|
|
302
|
+
|
|
303
|
+
const result = JSON.parse(normalized);
|
|
304
|
+
return { parsed: result };
|
|
305
|
+
} catch (e) {
|
|
306
|
+
try {
|
|
307
|
+
const result = extractFlatParams(raw);
|
|
308
|
+
if (result && Object.keys(result).length > 0) {
|
|
309
|
+
return { parsed: result };
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// fall through
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
parsed: null,
|
|
316
|
+
error: `Cannot parse params: ${(e as Error).message}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractFlatParams(raw: string): Record<string, unknown> | null {
|
|
322
|
+
const result: Record<string, unknown> = {};
|
|
323
|
+
const kvPattern = /(\w+)\s*:\s*(?:"([^"]*)"|\[([^\]]*)\]|(\{[^}]*\})|(\w+))/g;
|
|
324
|
+
let match;
|
|
325
|
+
let found = false;
|
|
326
|
+
|
|
327
|
+
while ((match = kvPattern.exec(raw)) !== null) {
|
|
328
|
+
found = true;
|
|
329
|
+
const key = match[1];
|
|
330
|
+
if (match[2] !== undefined) {
|
|
331
|
+
result[key] = match[2].replace(/<[^>]+>/g, 'placeholder');
|
|
332
|
+
} else if (match[3] !== undefined) {
|
|
333
|
+
result[key] = ['placeholder'];
|
|
334
|
+
} else if (match[4] !== undefined) {
|
|
335
|
+
result[key] = {};
|
|
336
|
+
} else if (match[5] !== undefined) {
|
|
337
|
+
const val = match[5];
|
|
338
|
+
if (val === 'true') result[key] = true;
|
|
339
|
+
else if (val === 'false') result[key] = false;
|
|
340
|
+
else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
|
|
341
|
+
else result[key] = val;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return found ? result : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Validation ──────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function isPlaceholder(value: unknown): boolean {
|
|
351
|
+
if (typeof value !== 'string') return false;
|
|
352
|
+
if (value.includes('|')) return true;
|
|
353
|
+
if (/^<.*>$/.test(value)) return true;
|
|
354
|
+
if (/^(placeholder|example|value|name|id|title|description|type|domain)$/i.test(value))
|
|
355
|
+
return true;
|
|
356
|
+
if (
|
|
357
|
+
/^[\w]+-[\w]+$/.test(value) &&
|
|
358
|
+
/\b(correct|your|my|the|this|some|new|old|entry|item|current)\b/i.test(value)
|
|
359
|
+
)
|
|
360
|
+
return true;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isPlaceholderIssue(
|
|
365
|
+
issue: { code: string; path: (string | number)[]; received?: unknown; message: string },
|
|
366
|
+
params: Record<string, unknown>,
|
|
367
|
+
): boolean {
|
|
368
|
+
if (issue.code === 'invalid_enum_value') {
|
|
369
|
+
const received = issue.received ?? getNestedValue(params, issue.path);
|
|
370
|
+
if (isPlaceholder(received)) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getNestedValue(obj: Record<string, unknown>, path: (string | number)[]): unknown {
|
|
376
|
+
let current: unknown = obj;
|
|
377
|
+
for (const key of path) {
|
|
378
|
+
if (current === null || current === undefined || typeof current !== 'object') return undefined;
|
|
379
|
+
current = (current as Record<string | number, unknown>)[key];
|
|
380
|
+
}
|
|
381
|
+
return current;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validateExamples(
|
|
385
|
+
examples: OpExample[],
|
|
386
|
+
registry: Map<string, ZodType>,
|
|
387
|
+
): SkillValidationError[] {
|
|
388
|
+
const errors: SkillValidationError[] = [];
|
|
389
|
+
|
|
390
|
+
for (const ex of examples) {
|
|
391
|
+
if (ex.parseError) continue;
|
|
392
|
+
if (!ex.parsedParams) continue;
|
|
393
|
+
|
|
394
|
+
const schema = registry.get(ex.opName);
|
|
395
|
+
if (!schema) {
|
|
396
|
+
errors.push({
|
|
397
|
+
file: ex.file,
|
|
398
|
+
op: ex.opName,
|
|
399
|
+
line: ex.line,
|
|
400
|
+
message: `unknown op — not found in any facade schema registry`,
|
|
401
|
+
});
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const result = (
|
|
406
|
+
schema as {
|
|
407
|
+
safeParse: (p: unknown) => { success: boolean; error?: ZodError };
|
|
408
|
+
}
|
|
409
|
+
).safeParse(ex.parsedParams);
|
|
410
|
+
|
|
411
|
+
if (!result.success && result.error) {
|
|
412
|
+
for (const issue of result.error.issues) {
|
|
413
|
+
if (
|
|
414
|
+
isPlaceholderIssue(
|
|
415
|
+
issue as {
|
|
416
|
+
code: string;
|
|
417
|
+
path: (string | number)[];
|
|
418
|
+
received?: unknown;
|
|
419
|
+
message: string;
|
|
420
|
+
},
|
|
421
|
+
ex.parsedParams,
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const path = issue.path.join('.');
|
|
427
|
+
errors.push({
|
|
428
|
+
file: ex.file,
|
|
429
|
+
op: ex.opName,
|
|
430
|
+
line: ex.line,
|
|
431
|
+
message: `${path ? path + ': ' : ''}${issue.message}`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return errors;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Validate all SKILL.md files found in `skillsDir`.
|
|
444
|
+
*
|
|
445
|
+
* `skillsDir` is the directory that contains skill subdirectories, e.g. ~/.claude/skills/.
|
|
446
|
+
* Each subdirectory is expected to have a SKILL.md file.
|
|
447
|
+
*
|
|
448
|
+
* @returns Structured result with errors, counts, and whether all examples are valid.
|
|
449
|
+
*/
|
|
450
|
+
export function validateSkillDocs(skillsDir: string): SkillValidationResult {
|
|
451
|
+
const registry = buildSchemaRegistry();
|
|
452
|
+
const skillFiles = discoverSkillFiles(skillsDir);
|
|
453
|
+
let totalExamples = 0;
|
|
454
|
+
const allErrors: SkillValidationError[] = [];
|
|
455
|
+
|
|
456
|
+
for (const file of skillFiles) {
|
|
457
|
+
const examples = extractOpExamples(file);
|
|
458
|
+
totalExamples += examples.length;
|
|
459
|
+
const errors = validateExamples(examples, registry);
|
|
460
|
+
allErrors.push(...errors);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
valid: allErrors.length === 0,
|
|
465
|
+
errors: allErrors,
|
|
466
|
+
totalFiles: skillFiles.length,
|
|
467
|
+
totalExamples,
|
|
468
|
+
registrySize: registry.size,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
@@ -36,6 +36,7 @@ describe('Telemetry', () => {
|
|
|
36
36
|
expect(stats.callsByOp).toEqual({});
|
|
37
37
|
expect(stats.errorsByOp).toEqual({});
|
|
38
38
|
expect(stats.slowestOps).toEqual([]);
|
|
39
|
+
expect(typeof stats.since).toBe('number');
|
|
39
40
|
expect(stats.since).toBeLessThanOrEqual(Date.now());
|
|
40
41
|
});
|
|
41
42
|
|
|
@@ -128,7 +128,10 @@ describe('HttpMcpServer', () => {
|
|
|
128
128
|
it('starts and stops without error', async () => {
|
|
129
129
|
await server.start();
|
|
130
130
|
const stats = server.getStats();
|
|
131
|
+
expect(typeof stats.uptime).toBe('number');
|
|
132
|
+
// uptime is elapsed ms since start — should be a small non-negative number
|
|
131
133
|
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
|
134
|
+
expect(stats.uptime).toBeLessThan(5000); // must have completed in under 5s
|
|
132
135
|
await server.stop();
|
|
133
136
|
});
|
|
134
137
|
|
|
@@ -30,11 +30,13 @@ describe('SessionManager', () => {
|
|
|
30
30
|
|
|
31
31
|
describe('add / get / remove', () => {
|
|
32
32
|
it('adds and retrieves a session', () => {
|
|
33
|
+
const before = Date.now();
|
|
33
34
|
const session = manager.add('s1', 'transport', 'server');
|
|
34
35
|
expect(session.id).toBe('s1');
|
|
35
36
|
expect(session.transport).toBe('transport');
|
|
36
37
|
expect(session.server).toBe('server');
|
|
37
|
-
expect(session.createdAt).
|
|
38
|
+
expect(session.createdAt).toBeGreaterThanOrEqual(before);
|
|
39
|
+
expect(session.createdAt).toBeLessThanOrEqual(Date.now());
|
|
38
40
|
expect(manager.get('s1')).toBe(session);
|
|
39
41
|
});
|
|
40
42
|
|
|
@@ -121,16 +121,13 @@ describe('loadToken / saveToken / getOrGenerateToken', () => {
|
|
|
121
121
|
expect(result).toBe('trimmed-token');
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('
|
|
124
|
+
it('skips whitespace-only env var and does not return the raw whitespace value', () => {
|
|
125
125
|
vi.stubEnv('MY_AGENT_HTTP_TOKEN', ' ');
|
|
126
|
+
// env var is whitespace-only — function must skip it and NOT return whitespace
|
|
126
127
|
const result = loadToken('my-agent');
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
it('generateToken produces different tokens each call', () => {
|
|
132
|
-
const t1 = generateToken();
|
|
133
|
-
const t2 = generateToken();
|
|
134
|
-
expect(t1).not.toBe(t2);
|
|
128
|
+
expect(result).not.toBe(' ');
|
|
129
|
+
if (result !== undefined) {
|
|
130
|
+
expect(result.trim().length).toBeGreaterThan(0);
|
|
131
|
+
}
|
|
135
132
|
});
|
|
136
133
|
});
|
|
@@ -124,7 +124,11 @@ describe('WsMcpServer', () => {
|
|
|
124
124
|
describe('standalone start / stop', () => {
|
|
125
125
|
it('starts and stops without error', async () => {
|
|
126
126
|
await server.start(0);
|
|
127
|
-
|
|
127
|
+
const uptime = server.getStats().uptime;
|
|
128
|
+
expect(typeof uptime).toBe('number');
|
|
129
|
+
// uptime is elapsed ms since start — should be a small non-negative number
|
|
130
|
+
expect(uptime).toBeGreaterThanOrEqual(0);
|
|
131
|
+
expect(uptime).toBeLessThan(5000); // must have started in under 5s
|
|
128
132
|
await server.stop();
|
|
129
133
|
});
|
|
130
134
|
|
|
@@ -145,7 +149,11 @@ describe('WsMcpServer', () => {
|
|
|
145
149
|
callbacks,
|
|
146
150
|
);
|
|
147
151
|
wsServer.attachTo(httpServer);
|
|
148
|
-
|
|
152
|
+
const attachUptime = wsServer.getStats().uptime;
|
|
153
|
+
expect(typeof attachUptime).toBe('number');
|
|
154
|
+
// uptime is elapsed ms since attach — valid non-negative number
|
|
155
|
+
expect(attachUptime).toBeGreaterThanOrEqual(0);
|
|
156
|
+
expect(attachUptime).toBeLessThan(5000);
|
|
149
157
|
|
|
150
158
|
await wsServer.stop();
|
|
151
159
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree reaper — cleans up stale .claude/worktrees/ entries left by subagent execution.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code creates worktrees via `isolation: "worktree"` for parallel subagent runs.
|
|
5
|
+
* If the agent commits changes, the worktree persists — nobody reaps it automatically.
|
|
6
|
+
*
|
|
7
|
+
* Usage: call worktreeReap() at session start and after plan completion (best-effort).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export interface ReapReport {
|
|
15
|
+
/** Number of worktrees successfully reaped */
|
|
16
|
+
reaped: number;
|
|
17
|
+
/** Paths of stale worktrees found */
|
|
18
|
+
found: string[];
|
|
19
|
+
/** Any errors encountered (non-fatal) */
|
|
20
|
+
errors: string[];
|
|
21
|
+
/** Whether git worktree prune ran successfully */
|
|
22
|
+
pruned: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorktreeStatus {
|
|
26
|
+
/** All .claude/worktrees/ entries found */
|
|
27
|
+
stale: Array<{ path: string; branch: string; commit: string }>;
|
|
28
|
+
/** Total count */
|
|
29
|
+
total: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse `git worktree list --porcelain` output into structured entries.
|
|
34
|
+
*/
|
|
35
|
+
function parseWorktreeList(
|
|
36
|
+
output: string,
|
|
37
|
+
): Array<{ path: string; branch: string; commit: string }> {
|
|
38
|
+
const entries: Array<{ path: string; branch: string; commit: string }> = [];
|
|
39
|
+
const blocks = output.trim().split(/\n\n+/);
|
|
40
|
+
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const lines = block.trim().split('\n');
|
|
43
|
+
let path = '';
|
|
44
|
+
let branch = '';
|
|
45
|
+
let commit = '';
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.startsWith('worktree ')) path = line.slice(9).trim();
|
|
49
|
+
else if (line.startsWith('HEAD ')) commit = line.slice(5).trim();
|
|
50
|
+
else if (line.startsWith('branch ')) branch = line.slice(7).trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (path) entries.push({ path, branch, commit });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get status of stale worktrees under .claude/worktrees/ without removing them.
|
|
61
|
+
*/
|
|
62
|
+
export function worktreeStatus(projectPath: string): WorktreeStatus {
|
|
63
|
+
const result = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
64
|
+
cwd: projectPath,
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.status !== 0 || !result.stdout) {
|
|
69
|
+
return { stale: [], total: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const all = parseWorktreeList(result.stdout);
|
|
73
|
+
const worktreeBase = join(projectPath, '.claude', 'worktrees');
|
|
74
|
+
const stale = all.filter((e) => e.path.startsWith(worktreeBase) && existsSync(e.path));
|
|
75
|
+
|
|
76
|
+
return { stale, total: stale.length };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reap stale worktrees under .claude/worktrees/.
|
|
81
|
+
* Best-effort — errors are collected but never thrown.
|
|
82
|
+
*/
|
|
83
|
+
export function worktreeReap(projectPath: string): ReapReport {
|
|
84
|
+
const report: ReapReport = { reaped: 0, found: [], errors: [], pruned: false };
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const { stale } = worktreeStatus(projectPath);
|
|
88
|
+
|
|
89
|
+
for (const { path } of stale) {
|
|
90
|
+
report.found.push(path);
|
|
91
|
+
const rm = spawnSync('git', ['worktree', 'remove', '--force', path], {
|
|
92
|
+
cwd: projectPath,
|
|
93
|
+
encoding: 'utf-8',
|
|
94
|
+
});
|
|
95
|
+
if (rm.status === 0) {
|
|
96
|
+
report.reaped++;
|
|
97
|
+
} else {
|
|
98
|
+
report.errors.push(`Failed to remove ${path}: ${rm.stderr?.trim() ?? 'unknown error'}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Prune dangling refs
|
|
103
|
+
const prune = spawnSync('git', ['worktree', 'prune'], {
|
|
104
|
+
cwd: projectPath,
|
|
105
|
+
encoding: 'utf-8',
|
|
106
|
+
});
|
|
107
|
+
report.pruned = prune.status === 0;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
report.errors.push(err instanceof Error ? err.message : String(err));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return report;
|
|
113
|
+
}
|